Compare commits

19 Commits
main ... main

Author SHA1 Message Date
7c30c1b3ba Merge remote-tracking branch 'origin/main' 2025-09-16 04:56:47 +01:00
ef20bb5f97 Make Roslyn shut up about long methods 2025-09-15 02:57:09 +01:00
5a6bf4e7c2 PGSQL Thread safety checks turned back on until we're confident; performance gain is negligible by having them off 2025-09-15 02:50:45 +01:00
919ab864ce Redis reads prefer replica 2025-09-15 00:48:15 +01:00
a741f25bde AccountRegistrationService now uses Redis to track registration throttling instead of spawning a bunch of async tasks 2025-09-14 23:40:58 +01:00
3aa4e62f28 Yeah this should just be a string.StartsWith. 2025-09-14 23:09:57 +01:00
afea311da6 Mores sensible timeout, it's just checking the start of a string (why is that even Regex?) 2025-09-14 22:48:15 +01:00
17a5354627 More Regex timeouts 2025-09-14 21:39:03 +01:00
9ea2a70c33 Add timeout to UA Regex evaluations 2025-09-14 21:23:14 +01:00
d23184b1ec Collision avoidance - UID length = 8, GID length = 10 2025-09-14 00:43:36 +01:00
f07fd3990b Using _redis multiplexer instead of DIing it 2025-09-14 00:40:45 +01:00
b987a58caa Redis Cluster prep 2025-09-14 00:26:59 +01:00
a314664c40 Package version bumps 2025-09-14 00:17:38 +01:00
d4a6949a50 Merge pull request #4 from ProfessorFartsalot/main
Fix connection issues once and for all
2025-09-09 21:54:41 +01:00
3b932420d3 Fix connection issues once and for all
+ Theory behind this is that on connect, they get a connection ID. On disconnect, that connection id is removed from their redis set. If they have no remaining keys, they're disposed, otherwise we can be sure they've reconnected and should not be disposed.
2025-09-09 14:54:18 -04:00
cbd7610809 Merge pull request #3 from ProfessorFartsalot/main
Fix health checks and add check for reconnects
2025-09-05 06:13:59 +01:00
271a6b0373 Optimise disconnect logic
Instead of trying to prevent stale connections, we just let the code remove them from existence. If they no longer exist, they'll get an error and reconnect.
2025-09-04 21:52:11 -04:00
74155ce171 Shorten async timeout window to 3000ms 2025-09-04 18:37:21 -04:00
ef4c463471 Fix health checks and add check for reconnects
Allows the client to auto reconnect when they encountered a connection issue.
2025-09-04 17:15:54 -04:00
22 changed files with 152 additions and 179 deletions

View File

@@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MessagePack.Annotations" Version="2.5.198" /> <PackageReference Include="MessagePack.Annotations" Version="3.1.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -2,3 +2,5 @@
# MA0048: File name must match type name # MA0048: File name must match type name
dotnet_diagnostic.MA0048.severity = suggestion dotnet_diagnostic.MA0048.severity = suggestion
# MA0051: Method too long
dotnet_diagnostic.MA0051.severity = none

View File

@@ -16,6 +16,7 @@ using StackExchange.Redis.Extensions.Core.Abstractions;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
using System.Text; using System.Text;
using StackExchange.Redis;
namespace MareSynchronosAuthService.Controllers; namespace MareSynchronosAuthService.Controllers;
@@ -112,7 +113,7 @@ public class JwtController : Controller
return Unauthorized("You are permanently banned."); return Unauthorized("You are permanently banned.");
} }
var existingIdent = await _redis.GetAsync<string>("UID:" + authResult.Uid); var existingIdent = await _redis.GetAsync<string>("UID:" + authResult.Uid, CommandFlags.PreferReplica);
if (!string.IsNullOrEmpty(existingIdent)) return Unauthorized("Already logged in to this account. Reconnect in 60 seconds. If you keep seeing this issue, restart your game."); if (!string.IsNullOrEmpty(existingIdent)) return Unauthorized("Already logged in to this account. Reconnect in 60 seconds. If you keep seeing this issue, restart your game.");
var token = CreateToken(new List<Claim>() var token = CreateToken(new List<Claim>()

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
@@ -23,12 +23,12 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.212"> <PackageReference Include="Meziantou.Analyzer" Version="2.0.219">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" /> <PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.9" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -8,21 +8,11 @@ using MareSynchronosShared.Utils.Configuration;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using MareSynchronosShared.Models; using MareSynchronosShared.Models;
using StackExchange.Redis;
using StackExchange.Redis.Extensions.Core.Abstractions;
namespace MareSynchronosAuthService.Services; namespace MareSynchronosAuthService.Services;
internal record IpRegistrationCount
{
private int count = 1;
public int Count => count;
public Task ResetTask { get; set; }
public CancellationTokenSource ResetTaskCts { get; set; }
public void IncreaseCount()
{
Interlocked.Increment(ref count);
}
}
public class AccountRegistrationService public class AccountRegistrationService
{ {
private readonly MareMetrics _metrics; private readonly MareMetrics _metrics;
@@ -30,52 +20,35 @@ public class AccountRegistrationService
private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IConfigurationService<AuthServiceConfiguration> _configurationService; private readonly IConfigurationService<AuthServiceConfiguration> _configurationService;
private readonly ILogger<AccountRegistrationService> _logger; private readonly ILogger<AccountRegistrationService> _logger;
private readonly ConcurrentDictionary<string, IpRegistrationCount> _registrationsPerIp = new(StringComparer.Ordinal); private readonly IRedisDatabase _redis;
private Regex _registrationUserAgentRegex = new Regex(@"^MareSynchronos/", RegexOptions.Compiled);
public AccountRegistrationService(MareMetrics metrics, MareDbContext mareDbContext, public AccountRegistrationService(MareMetrics metrics, MareDbContext mareDbContext,
IServiceScopeFactory serviceScopeFactory, IConfigurationService<AuthServiceConfiguration> configuration, IServiceScopeFactory serviceScopeFactory, IConfigurationService<AuthServiceConfiguration> configuration,
ILogger<AccountRegistrationService> logger) ILogger<AccountRegistrationService> logger, IRedisDatabase redisDb)
{ {
_mareDbContext = mareDbContext; _mareDbContext = mareDbContext;
_logger = logger; _logger = logger;
_configurationService = configuration; _configurationService = configuration;
_metrics = metrics; _metrics = metrics;
_serviceScopeFactory = serviceScopeFactory; _serviceScopeFactory = serviceScopeFactory;
_redis = redisDb;
} }
public async Task<RegisterReplyV2Dto> RegisterAccountAsync(string ua, string ip, string hashedSecretKey) public async Task<RegisterReplyV2Dto> RegisterAccountAsync(string ua, string ip, string hashedSecretKey)
{ {
var reply = new RegisterReplyV2Dto(); var reply = new RegisterReplyV2Dto();
if (!_registrationUserAgentRegex.Match(ua).Success) if (string.IsNullOrEmpty(ua) || !ua.StartsWith("MareSynchronos/", StringComparison.Ordinal))
{ {
reply.ErrorMessage = "User-Agent not allowed"; reply.ErrorMessage = "User-Agent not allowed";
return reply; return reply;
} }
if (_registrationsPerIp.TryGetValue(ip, out var registrationCount) var registrationsByIp = await _redis.GetAsync<int>("IPREG:" + ip, CommandFlags.PreferReplica).ConfigureAwait(false);
&& registrationCount.Count >= _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.RegisterIpLimit), 3)) if (registrationsByIp >= _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.RegisterIpLimit), 3))
{ {
_logger.LogWarning("Rejecting {ip} for registration spam", ip);
if (registrationCount.ResetTask == null)
{
registrationCount.ResetTaskCts = new CancellationTokenSource();
if (registrationCount.ResetTaskCts != null)
registrationCount.ResetTaskCts.Cancel();
registrationCount.ResetTask = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMinutes(_configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.RegisterIpDurationInMinutes), 10))).ConfigureAwait(false);
}).ContinueWith((t) =>
{
_registrationsPerIp.Remove(ip, out _);
}, registrationCount.ResetTaskCts.Token);
}
reply.ErrorMessage = "Too many registrations from this IP. Please try again later."; reply.ErrorMessage = "Too many registrations from this IP. Please try again later.";
return reply; return reply;
} }
@@ -85,18 +58,12 @@ public class AccountRegistrationService
var hasValidUid = false; var hasValidUid = false;
while (!hasValidUid) while (!hasValidUid)
{ {
var uid = StringUtils.GenerateRandomString(7); var uid = StringUtils.GenerateRandomString(8);
if (_mareDbContext.Users.Any(u => u.UID == uid || u.Alias == uid)) continue; if (_mareDbContext.Users.Any(u => u.UID == uid || u.Alias == uid)) continue;
user.UID = uid; user.UID = uid;
hasValidUid = true; hasValidUid = true;
} }
// make the first registered user on the service to admin
if (!await _mareDbContext.Users.AnyAsync().ConfigureAwait(false))
{
user.IsAdmin = true;
}
user.LastLoggedIn = DateTime.UtcNow; user.LastLoggedIn = DateTime.UtcNow;
var auth = new Auth() var auth = new Auth()
@@ -115,38 +82,14 @@ public class AccountRegistrationService
reply.Success = true; reply.Success = true;
reply.UID = user.UID; reply.UID = user.UID;
RecordIpRegistration(ip);
await _redis.Database.StringIncrementAsync($"IPREG:{ip}").ConfigureAwait(false);
// Naive implementation, but should be good enough. A true sliding window *probably* isn't necessary.
await _redis.Database.KeyExpireAsync($"IPREG:{ip}", TimeSpan.
FromMinutes(_configurationService.GetValueOrDefault(nameof(
AuthServiceConfiguration.RegisterIpDurationInMinutes), 60))).
ConfigureAwait(false);
return reply; return reply;
} }
private void RecordIpRegistration(string ip)
{
var whitelisted = _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.WhitelistedIps), new List<string>());
if (!whitelisted.Any(w => ip.Contains(w, StringComparison.OrdinalIgnoreCase)))
{
if (_registrationsPerIp.TryGetValue(ip, out var count))
{
count.IncreaseCount();
}
else
{
count = _registrationsPerIp[ip] = new IpRegistrationCount();
if (count.ResetTaskCts != null)
count.ResetTaskCts.Cancel();
count.ResetTaskCts = new CancellationTokenSource();
count.ResetTask = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMinutes(_configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.RegisterIpDurationInMinutes), 10))).ConfigureAwait(false);
}).ContinueWith((t) =>
{
_registrationsPerIp.Remove(ip, out _);
}, count.ResetTaskCts.Token);
}
}
}
} }

View File

@@ -165,19 +165,22 @@ public class Startup
var options = ConfigurationOptions.Parse(redisConnection); var options = ConfigurationOptions.Parse(redisConnection);
var endpoint = options.EndPoints[0]; var hosts = options.EndPoints
string address = ""; .Select(ep =>
int port = 0; {
if (endpoint is DnsEndPoint dnsEndPoint) { address = dnsEndPoint.Host; port = dnsEndPoint.Port; } if (ep is DnsEndPoint dns) return (host: dns.Host, port: dns.Port);
if (endpoint is IPEndPoint ipEndPoint) { address = ipEndPoint.Address.ToString(); port = ipEndPoint.Port; } if (ep is IPEndPoint ip) return (host: ip.Address.ToString(), port: ip.Port);
return (host: (string?)null, port: 0);
})
.Where(x => x.host != null)
.Distinct()
.Select(x => new RedisHost { Host = x.host!, Port = x.port })
.ToArray();
var redisConfiguration = new RedisConfiguration() var redisConfiguration = new RedisConfiguration()
{ {
AbortOnConnectFail = true, AbortOnConnectFail = false,
KeyPrefix = "", KeyPrefix = "",
Hosts = new RedisHost[] Hosts = hosts,
{
new RedisHost(){ Host = address, Port = port },
},
AllowAdmin = true, AllowAdmin = true,
ConnectTimeout = options.ConnectTimeout, ConnectTimeout = options.ConnectTimeout,
Database = 0, Database = 0,
@@ -187,11 +190,11 @@ public class Startup
{ {
Mode = ServerEnumerationStrategy.ModeOptions.All, Mode = ServerEnumerationStrategy.ModeOptions.All,
TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any, TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any,
UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.Throw, UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.IgnoreIfOtherAvailable,
}, },
MaxValueLength = 1024, MaxValueLength = 1024,
PoolSize = mareConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50), PoolSize = mareConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50),
SyncTimeout = options.SyncTimeout, SyncTimeout = Math.Max(options.SyncTimeout, 10000),
}; };
services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(redisConfiguration); services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(redisConfiguration);
@@ -211,7 +214,7 @@ public class Startup
builder.MigrationsHistoryTable("_efmigrationshistory", "public"); builder.MigrationsHistoryTable("_efmigrationshistory", "public");
builder.MigrationsAssembly("MareSynchronosShared"); builder.MigrationsAssembly("MareSynchronosShared");
}).UseSnakeCaseNamingConvention(); }).UseSnakeCaseNamingConvention();
options.EnableThreadSafetyChecks(false); options.EnableThreadSafetyChecks();
}, mareConfig.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024)); }, mareConfig.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024));
services.AddDbContextFactory<MareDbContext>(options => services.AddDbContextFactory<MareDbContext>(options =>
{ {
@@ -220,7 +223,7 @@ public class Startup
builder.MigrationsHistoryTable("_efmigrationshistory", "public"); builder.MigrationsHistoryTable("_efmigrationshistory", "public");
builder.MigrationsAssembly("MareSynchronosShared"); builder.MigrationsAssembly("MareSynchronosShared");
}).UseSnakeCaseNamingConvention(); }).UseSnakeCaseNamingConvention();
options.EnableThreadSafetyChecks(false); options.EnableThreadSafetyChecks();
}); });
} }
} }

View File

@@ -6,6 +6,7 @@ using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Group; using MareSynchronos.API.Dto.Group;
using MareSynchronosShared.Metrics; using MareSynchronosShared.Metrics;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using StackExchange.Redis;
namespace MareSynchronosServer.Hubs; namespace MareSynchronosServer.Hubs;
@@ -118,14 +119,14 @@ public partial class MareHub
private async Task<Dictionary<string, string>> GetOnlineUsers(List<string> uids) private async Task<Dictionary<string, string>> GetOnlineUsers(List<string> uids)
{ {
var result = await _redis.GetAllAsync<string>(uids.Select(u => "UID:" + u).ToHashSet(StringComparer.Ordinal)).ConfigureAwait(false); var result = await _redis.GetAllAsync<string>(uids.Select(u => "UID:" + u).ToHashSet(StringComparer.Ordinal), CommandFlags.PreferReplica).ConfigureAwait(false);
return uids.Where(u => result.TryGetValue("UID:" + u, out var ident) && !string.IsNullOrEmpty(ident)).ToDictionary(u => u, u => result["UID:" + u], StringComparer.Ordinal); return uids.Where(u => result.TryGetValue("UID:" + u, out var ident) && !string.IsNullOrEmpty(ident)).ToDictionary(u => u, u => result["UID:" + u], StringComparer.Ordinal);
} }
private async Task<string> GetUserIdent(string uid) private async Task<string> GetUserIdent(string uid)
{ {
if (string.IsNullOrEmpty(uid)) return string.Empty; if (string.IsNullOrEmpty(uid)) return string.Empty;
return await _redis.GetAsync<string>("UID:" + uid).ConfigureAwait(false); return await _redis.GetAsync<string>("UID:" + uid, CommandFlags.PreferReplica).ConfigureAwait(false);
} }
private async Task RemoveUserFromRedis() private async Task RemoveUserFromRedis()
@@ -202,6 +203,8 @@ public partial class MareHub
private async Task UpdateUserOnRedis() private async Task UpdateUserOnRedis()
{ {
await _redis.AddAsync("UID:" + UserUID, UserCharaIdent, TimeSpan.FromSeconds(60), StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); await _redis.AddAsync("UID:" + UserUID, UserCharaIdent, TimeSpan.FromSeconds(60), StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false);
await _redis.SetAddAsync($"connections:{UserCharaIdent}", Context.ConnectionId).ConfigureAwait(false);
await _redis.UpdateExpiryAsync($"connections:{UserCharaIdent}", TimeSpan.FromSeconds(60)).ConfigureAwait(false);
} }
private async Task UserGroupLeave(GroupPair groupUserPair, List<PausedEntry> allUserPairs, string userIdent, string? uid = null) private async Task UserGroupLeave(GroupPair groupUserPair, List<PausedEntry> allUserPairs, string userIdent, string? uid = null)

View File

@@ -6,6 +6,7 @@ using MareSynchronosShared.Utils;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
namespace MareSynchronosServer.Hubs; namespace MareSynchronosServer.Hubs;
@@ -13,12 +14,12 @@ public partial class MareHub
{ {
private async Task<string?> GetUserGposeLobby() private async Task<string?> GetUserGposeLobby()
{ {
return await _redis.GetAsync<string>(GposeLobbyUser).ConfigureAwait(false); return await _redis.GetAsync<string>(GposeLobbyUser, CommandFlags.PreferReplica).ConfigureAwait(false);
} }
private async Task<List<string>> GetUsersInLobby(string lobbyId, bool includeSelf = false) private async Task<List<string>> GetUsersInLobby(string lobbyId, bool includeSelf = false)
{ {
var users = await _redis.GetAsync<List<string>>($"GposeLobby:{lobbyId}").ConfigureAwait(false); var users = await _redis.GetAsync<List<string>>($"GposeLobby:{lobbyId}", CommandFlags.PreferReplica).ConfigureAwait(false);
return users?.Where(u => includeSelf || !string.Equals(u, UserUID, StringComparison.Ordinal)).ToList() ?? []; return users?.Where(u => includeSelf || !string.Equals(u, UserUID, StringComparison.Ordinal)).ToList() ?? [];
} }
@@ -68,7 +69,7 @@ public partial class MareHub
while (string.IsNullOrEmpty(lobbyId)) while (string.IsNullOrEmpty(lobbyId))
{ {
lobbyId = StringUtils.GenerateRandomString(30, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); lobbyId = StringUtils.GenerateRandomString(30, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
var result = await _redis.GetAsync<List<string>>($"GposeLobby:{lobbyId}").ConfigureAwait(false); var result = await _redis.GetAsync<List<string>>($"GposeLobby:{lobbyId}", CommandFlags.PreferReplica).ConfigureAwait(false);
if (result != null) if (result != null)
lobbyId = string.Empty; lobbyId = string.Empty;
} }

View File

@@ -212,10 +212,10 @@ public partial class MareHub
throw new System.Exception($"Max groups for user is {_maxExistingGroupsByUser}, max joined groups is {_maxJoinedGroupsByUser}."); throw new System.Exception($"Max groups for user is {_maxExistingGroupsByUser}, max joined groups is {_maxJoinedGroupsByUser}.");
} }
var gid = StringUtils.GenerateRandomString(9); var gid = StringUtils.GenerateRandomString(10);
while (await DbContext.Groups.AnyAsync(g => g.GID == "SNOW-" + gid).ConfigureAwait(false)) while (await DbContext.Groups.AnyAsync(g => g.GID == "SNOW-" + gid).ConfigureAwait(false))
{ {
gid = StringUtils.GenerateRandomString(9); gid = StringUtils.GenerateRandomString(10);
} }
gid = "SNOW-" + gid; gid = "SNOW-" + gid;

View File

@@ -500,13 +500,13 @@ public partial class MareHub
await Clients.Caller.Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false); await Clients.Caller.Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false);
} }
[GeneratedRegex(@"^([a-z0-9_ '+&,\.\-\{\}]+\/)+([a-z0-9_ '+&,\.\-\{\}]+\.[a-z]{3,4})$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript)] [GeneratedRegex(@"^([a-z0-9_ '+&,\.\-\{\}]+\/)+([a-z0-9_ '+&,\.\-\{\}]+\.[a-z]{3,4})$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript, matchTimeoutMilliseconds: 1000) ]
private static partial Regex GamePathRegex(); private static partial Regex GamePathRegex();
[GeneratedRegex(@"^[A-Z0-9]{40}$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript)] [GeneratedRegex(@"^[A-Z0-9]{40}$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript, matchTimeoutMilliseconds: 1000)]
private static partial Regex HashRegex(); private static partial Regex HashRegex();
[GeneratedRegex("^[-a-zA-Z0-9@:%._\\+~#=]{1,256}[\\.,][a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)$")] [GeneratedRegex("^[-a-zA-Z0-9@:%._\\+~#=]{1,256}[\\.,][a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)$", RegexOptions.None, matchTimeoutMilliseconds: 1000)]
private static partial Regex UrlRegex(); private static partial Regex UrlRegex();
private ClientPair OppositeEntry(string otherUID) => private ClientPair OppositeEntry(string otherUID) =>

View File

@@ -81,7 +81,7 @@ public partial class MareHub : Hub<IMareHub>, IMareHub
await DbContext.SaveChangesAsync().ConfigureAwait(false); await DbContext.SaveChangesAsync().ConfigureAwait(false);
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Welcome to Snowcloak! Current Online Users: " + _systemInfoService.SystemInfoDto.OnlineUsers).ConfigureAwait(false); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Welcome to Snowcloak! Current Online Users: " + _systemInfoService.SystemInfoDto.OnlineUsers).ConfigureAwait(false);
Context.Items["IsClientConnected"] = true;
return new ConnectionDto(new UserData(dbUser.UID, string.IsNullOrWhiteSpace(dbUser.Alias) ? null : dbUser.Alias)) return new ConnectionDto(new UserData(dbUser.UID, string.IsNullOrWhiteSpace(dbUser.Alias) ? null : dbUser.Alias))
{ {
CurrentClientVersion = _expectedClientVersion, CurrentClientVersion = _expectedClientVersion,
@@ -103,9 +103,16 @@ public partial class MareHub : Hub<IMareHub>, IMareHub
[Authorize(Policy = "Authenticated")] [Authorize(Policy = "Authenticated")]
public async Task<bool> CheckClientHealth() public async Task<bool> CheckClientHealth()
{ {
await UpdateUserOnRedis().ConfigureAwait(false); // Key missing -> health check failed!
var exists = await _redis.ExistsAsync("UID:" + UserUID).ConfigureAwait(false);
if (!exists)
{
return false;
}
return false; // Key exists -> health is good!
await UpdateUserOnRedis().ConfigureAwait(false);
return true;
} }
[Authorize(Policy = "Authenticated")] [Authorize(Policy = "Authenticated")]
@@ -115,8 +122,7 @@ public partial class MareHub : Hub<IMareHub>, IMareHub
try try
{ {
_logger.LogCallInfo(MareHubLogger.Args(_contextAccessor.GetIpAddress(), UserCharaIdent)); _logger.LogCallInfo(MareHubLogger.Args("A client reconnected.", _contextAccessor.GetIpAddress(), UserCharaIdent));
await UpdateUserOnRedis().ConfigureAwait(false); await UpdateUserOnRedis().ConfigureAwait(false);
} }
catch { } catch { }
@@ -128,17 +134,19 @@ public partial class MareHub : Hub<IMareHub>, IMareHub
public override async Task OnDisconnectedAsync(Exception exception) public override async Task OnDisconnectedAsync(Exception exception)
{ {
_mareMetrics.DecGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent); _mareMetrics.DecGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent);
try try
{ {
_logger.LogCallInfo(MareHubLogger.Args(_contextAccessor.GetIpAddress(), UserCharaIdent)); _logger.LogCallInfo(MareHubLogger.Args(_contextAccessor.GetIpAddress(), UserCharaIdent));
if (exception != null) if (exception != null)
_logger.LogCallWarning(MareHubLogger.Args(_contextAccessor.GetIpAddress(), exception.Message, exception.StackTrace)); _logger.LogCallWarning(MareHubLogger.Args(_contextAccessor.GetIpAddress(), exception.Message, exception.StackTrace));
await _redis.SetRemoveAsync($"connections:{UserCharaIdent}", Context.ConnectionId).ConfigureAwait(false);
await GposeLobbyLeave().ConfigureAwait(false); var connections = await _redis.SetMembersAsync<string>($"connections:{UserCharaIdent}").ConfigureAwait(false);
await RemoveUserFromRedis().ConfigureAwait(false); if (connections.Length == 0)
{
await SendOfflineToAllPairedUsers().ConfigureAwait(false); await GposeLobbyLeave().ConfigureAwait(false);
await RemoveUserFromRedis().ConfigureAwait(false);
await SendOfflineToAllPairedUsers().ConfigureAwait(false);
}
} }
catch { } catch { }

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<UserSecretsId>aspnet-MareSynchronosServer-BA82A12A-0B30-463C-801D-B7E81318CD50</UserSecretsId> <UserSecretsId>aspnet-MareSynchronosServer-BA82A12A-0B30-463C-801D-B7E81318CD50</UserSecretsId>
<AssemblyVersion>1.1.0.0</AssemblyVersion> <AssemblyVersion>1.1.0.0</AssemblyVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
@@ -25,12 +25,12 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.212"> <PackageReference Include="Meziantou.Analyzer" Version="2.0.219">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.9" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="7.7.1" /> <PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.14.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
</ItemGroup> </ItemGroup>

View File

@@ -3,6 +3,7 @@ using MareSynchronos.API.SignalR;
using MareSynchronosServer.Hubs; using MareSynchronosServer.Hubs;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using StackExchange.Redis.Extensions.Core.Abstractions; using StackExchange.Redis.Extensions.Core.Abstractions;
using StackExchange.Redis;
namespace MareSynchronosServer.Services; namespace MareSynchronosServer.Services;
@@ -153,7 +154,7 @@ public sealed class GPoseLobbyDistributionService : IHostedService, IDisposable
if (!lobbyId.Value.Values.Any()) if (!lobbyId.Value.Values.Any())
continue; continue;
var gposeLobbyUsers = await _redisDb.GetAsync<List<string>>($"GposeLobby:{lobbyId.Key}").ConfigureAwait(false); var gposeLobbyUsers = await _redisDb.GetAsync<List<string>>($"GposeLobby:{lobbyId.Key}", CommandFlags.PreferReplica).ConfigureAwait(false);
if (gposeLobbyUsers == null) if (gposeLobbyUsers == null)
continue; continue;
@@ -200,7 +201,7 @@ public sealed class GPoseLobbyDistributionService : IHostedService, IDisposable
if (!lobbyId.Value.Values.Any()) if (!lobbyId.Value.Values.Any())
continue; continue;
var gposeLobbyUsers = await _redisDb.GetAsync<List<string>>($"GposeLobby:{lobbyId.Key}").ConfigureAwait(false); var gposeLobbyUsers = await _redisDb.GetAsync<List<string>>($"GposeLobby:{lobbyId.Key}", CommandFlags.PreferReplica).ConfigureAwait(false);
if (gposeLobbyUsers == null) if (gposeLobbyUsers == null)
continue; continue;

View File

@@ -7,6 +7,7 @@ using MareSynchronosShared.Services;
using MareSynchronosShared.Utils.Configuration; using MareSynchronosShared.Utils.Configuration;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
using StackExchange.Redis.Extensions.Core.Abstractions; using StackExchange.Redis.Extensions.Core.Abstractions;
namespace MareSynchronosServer.Services; namespace MareSynchronosServer.Services;
@@ -19,6 +20,7 @@ public class SystemInfoService : IHostedService, IDisposable
private readonly ILogger<SystemInfoService> _logger; private readonly ILogger<SystemInfoService> _logger;
private readonly IHubContext<MareHub, IMareHub> _hubContext; private readonly IHubContext<MareHub, IMareHub> _hubContext;
private readonly IRedisDatabase _redis; private readonly IRedisDatabase _redis;
private readonly IConnectionMultiplexer _connectionMultiplexer;
private Timer _timer; private Timer _timer;
public SystemInfoDto SystemInfoDto { get; private set; } = new(); public SystemInfoDto SystemInfoDto { get; private set; } = new();
@@ -31,6 +33,7 @@ public class SystemInfoService : IHostedService, IDisposable
_logger = logger; _logger = logger;
_hubContext = hubContext; _hubContext = hubContext;
_redis = redisDb; _redis = redisDb;
_connectionMultiplexer = _redis.Database.Multiplexer;
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
@@ -52,8 +55,9 @@ public class SystemInfoService : IHostedService, IDisposable
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableWorkerThreads, workerThreads); _mareMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableWorkerThreads, workerThreads);
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableIOWorkerThreads, ioThreads); _mareMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableIOWorkerThreads, ioThreads);
var onlineUsers = _connectionMultiplexer.GetServers().Where(server => !server.IsReplica).SelectMany(server => server.Keys(0, "UID:*", pageSize:1000)).Count();
var onlineUsers = (_redis.SearchKeysAsync("UID:*").GetAwaiter().GetResult()).Count();
SystemInfoDto = new SystemInfoDto() SystemInfoDto = new SystemInfoDto()
{ {
OnlineUsers = onlineUsers, OnlineUsers = onlineUsers,
@@ -63,7 +67,7 @@ public class SystemInfoService : IHostedService, IDisposable
{ {
_logger.LogTrace("Sending System Info, Online Users: {onlineUsers}", onlineUsers); _logger.LogTrace("Sending System Info, Online Users: {onlineUsers}", onlineUsers);
_hubContext.Clients.All.Client_UpdateSystemInfo(SystemInfoDto); _ = _hubContext.Clients.All.Client_UpdateSystemInfo(SystemInfoDto);
using var scope = _services.CreateScope(); using var scope = _services.CreateScope();
using var db = scope.ServiceProvider.GetService<MareDbContext>()!; using var db = scope.ServiceProvider.GetService<MareDbContext>()!;

View File

@@ -142,19 +142,22 @@ public class Startup
var options = ConfigurationOptions.Parse(redisConnection); var options = ConfigurationOptions.Parse(redisConnection);
var endpoint = options.EndPoints[0]; var hosts = options.EndPoints
string address = ""; .Select(ep =>
int port = 0; {
if (endpoint is DnsEndPoint dnsEndPoint) { address = dnsEndPoint.Host; port = dnsEndPoint.Port; } if (ep is DnsEndPoint dns) return (host: dns.Host, port: dns.Port);
if (endpoint is IPEndPoint ipEndPoint) { address = ipEndPoint.Address.ToString(); port = ipEndPoint.Port; } if (ep is IPEndPoint ip) return (host: ip.Address.ToString(), port: ip.Port);
return (host: (string?)null, port: 0);
})
.Where(x => x.host != null)
.Distinct()
.Select(x => new RedisHost { Host = x.host!, Port = x.port })
.ToArray();
var redisConfiguration = new RedisConfiguration() var redisConfiguration = new RedisConfiguration()
{ {
AbortOnConnectFail = true, AbortOnConnectFail = false,
KeyPrefix = "", KeyPrefix = "",
Hosts = new RedisHost[] Hosts = hosts,
{
new RedisHost(){ Host = address, Port = port },
},
AllowAdmin = true, AllowAdmin = true,
ConnectTimeout = options.ConnectTimeout, ConnectTimeout = options.ConnectTimeout,
Database = 0, Database = 0,
@@ -164,11 +167,11 @@ public class Startup
{ {
Mode = ServerEnumerationStrategy.ModeOptions.All, Mode = ServerEnumerationStrategy.ModeOptions.All,
TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any, TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any,
UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.Throw, UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.IgnoreIfOtherAvailable,
}, },
MaxValueLength = 1024, MaxValueLength = 1024,
PoolSize = mareConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50), PoolSize = mareConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50),
SyncTimeout = options.SyncTimeout, SyncTimeout = Math.Max(options.SyncTimeout, 10000),
}; };
services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(redisConfiguration); services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(redisConfiguration);
@@ -242,7 +245,7 @@ public class Startup
builder.MigrationsHistoryTable("_efmigrationshistory", "public"); builder.MigrationsHistoryTable("_efmigrationshistory", "public");
builder.MigrationsAssembly("MareSynchronosShared"); builder.MigrationsAssembly("MareSynchronosShared");
}).UseSnakeCaseNamingConvention(); }).UseSnakeCaseNamingConvention();
options.EnableThreadSafetyChecks(false); options.EnableThreadSafetyChecks();
}, mareConfig.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024)); }, mareConfig.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024));
services.AddDbContextFactory<MareDbContext>(options => services.AddDbContextFactory<MareDbContext>(options =>
{ {
@@ -251,7 +254,7 @@ public class Startup
builder.MigrationsHistoryTable("_efmigrationshistory", "public"); builder.MigrationsHistoryTable("_efmigrationshistory", "public");
builder.MigrationsAssembly("MareSynchronosShared"); builder.MigrationsAssembly("MareSynchronosShared");
}).UseSnakeCaseNamingConvention(); }).UseSnakeCaseNamingConvention();
options.EnableThreadSafetyChecks(false); options.EnableThreadSafetyChecks();
}); });
} }

View File

@@ -82,7 +82,7 @@ public class MareModule : InteractionModuleBase
var hasValidUid = false; var hasValidUid = false;
while (!hasValidUid) while (!hasValidUid)
{ {
var uid = StringUtils.GenerateRandomString(7); var uid = StringUtils.GenerateRandomString(8);
if (db.Users.Any(u => u.UID == uid || u.Alias == uid)) continue; if (db.Users.Any(u => u.UID == uid || u.Alias == uid)) continue;
user.UID = uid; user.UID = uid;
hasValidUid = true; hasValidUid = true;
@@ -510,7 +510,7 @@ public class MareModule : InteractionModuleBase
var hasValidUid = false; var hasValidUid = false;
while (!hasValidUid) while (!hasValidUid)
{ {
var uid = StringUtils.GenerateRandomString(7); var uid = StringUtils.GenerateRandomString(8);
if (await db.Users.AnyAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false)) continue; if (await db.Users.AnyAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false)) continue;
newUser.UID = uid; newUser.UID = uid;
hasValidUid = true; hasValidUid = true;
@@ -646,7 +646,7 @@ public class MareModule : InteractionModuleBase
var auth = await db.Auth.Include(u => u.PrimaryUser).SingleOrDefaultAsync(u => u.UserUID == dbUser.UID).ConfigureAwait(false); var auth = await db.Auth.Include(u => u.PrimaryUser).SingleOrDefaultAsync(u => u.UserUID == dbUser.UID).ConfigureAwait(false);
var groups = await db.Groups.Where(g => g.OwnerUID == dbUser.UID).ToListAsync().ConfigureAwait(false); var groups = await db.Groups.Where(g => g.OwnerUID == dbUser.UID).ToListAsync().ConfigureAwait(false);
var groupsJoined = await db.GroupPairs.Where(g => g.GroupUserUID == dbUser.UID).ToListAsync().ConfigureAwait(false); var groupsJoined = await db.GroupPairs.Where(g => g.GroupUserUID == dbUser.UID).ToListAsync().ConfigureAwait(false);
var identity = await _connectionMultiplexer.GetDatabase().StringGetAsync("UID:" + dbUser.UID).ConfigureAwait(false); var identity = await _connectionMultiplexer.GetDatabase().StringGetAsync("UID:" + dbUser.UID, CommandFlags.PreferReplica).ConfigureAwait(false);
eb.WithTitle("User Information"); eb.WithTitle("User Information");
eb.WithDescription("This is the user information for Discord User <@" + userToCheckForDiscordId + ">" + Environment.NewLine + Environment.NewLine eb.WithDescription("This is the user information for Discord User <@" + userToCheckForDiscordId + ">" + Environment.NewLine + Environment.NewLine
@@ -1195,7 +1195,7 @@ public class MareModule : InteractionModuleBase
var hasValidUid = false; var hasValidUid = false;
while (!hasValidUid) while (!hasValidUid)
{ {
var uid = StringUtils.GenerateRandomString(7); var uid = StringUtils.GenerateRandomString(8);
if (db.Users.Any(u => u.UID == uid || u.Alias == uid)) continue; if (db.Users.Any(u => u.UID == uid || u.Alias == uid)) continue;
user.UID = uid; user.UID = uid;
hasValidUid = true; hasValidUid = true;

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
@@ -22,12 +22,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Discord.Net" Version="3.18.0" /> <PackageReference Include="Discord.Net" Version="3.18.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.212"> <PackageReference Include="Meziantou.Analyzer" Version="2.0.219">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.9" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -45,7 +45,7 @@ public class Startup
{ {
builder.MigrationsHistoryTable("_efmigrationshistory", "public"); builder.MigrationsHistoryTable("_efmigrationshistory", "public");
}).UseSnakeCaseNamingConvention(); }).UseSnakeCaseNamingConvention();
options.EnableThreadSafetyChecks(false); options.EnableThreadSafetyChecks();
}, Configuration.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024)); }, Configuration.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024));
services.AddSingleton(m => new MareMetrics(m.GetService<ILogger<MareMetrics>>(), new List<string> { }, services.AddSingleton(m => new MareMetrics(m.GetService<ILogger<MareMetrics>>(), new List<string> { },

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
@@ -15,13 +15,14 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="ByteSize" Version="2.1.2" /> <PackageReference Include="ByteSize" Version="2.1.2" />
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" /> <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
<PackageReference Include="IDisposableAnalyzers" Version="4.0.8"> <PackageReference Include="IDisposableAnalyzers" Version="4.0.8">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Karambolo.Extensions.Logging.File" Version="3.6.3" /> <PackageReference Include="Karambolo.Extensions.Logging.File" Version="3.6.3" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.212"> <PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.219">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@@ -30,25 +31,25 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.3.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Core" Version="2.3.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Core" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.19" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="8.0.19" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="9.0.9" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.19" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.19" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.19" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.19"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.9">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.19" /> <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.9" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="7.7.1" /> <PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.14.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="prometheus-net" Version="8.2.1" /> <PackageReference Include="prometheus-net" Version="8.2.1" />
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" /> <PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageReference Include="StackExchange.Redis" Version="2.9.11" /> <PackageReference Include="StackExchange.Redis" Version="2.9.17" />
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="10.2.0" /> <PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="11.0.0" />
<PackageReference Include="StackExchange.Redis.Extensions.Core" Version="10.2.0" /> <PackageReference Include="StackExchange.Redis.Extensions.Core" Version="11.0.0" />
<PackageReference Include="StackExchange.Redis.Extensions.System.Text.Json" Version="10.2.0" /> <PackageReference Include="StackExchange.Redis.Extensions.System.Text.Json" Version="11.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.7.1" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.3" /> <PackageReference Include="System.Linq.Async" Version="6.0.3" />
</ItemGroup> </ItemGroup>

View File

@@ -30,7 +30,7 @@ public class UserRequirementHandler : AuthorizationHandler<UserRequirement, HubI
if ((requirement.Requirements & UserRequirements.Identified) is UserRequirements.Identified) if ((requirement.Requirements & UserRequirements.Identified) is UserRequirements.Identified)
{ {
var ident = await _redis.GetAsync<string>("UID:" + uid).ConfigureAwait(false); var ident = await _redis.GetAsync<string>("UID:" + uid, CommandFlags.PreferReplica).ConfigureAwait(false);
if (ident == RedisValue.EmptyString) context.Fail(); if (ident == RedisValue.EmptyString) context.Fail();
} }

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
@@ -23,11 +23,11 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="K4os.Compression.LZ4.Streams" Version="1.3.8" /> <PackageReference Include="K4os.Compression.LZ4.Streams" Version="1.3.8" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.212"> <PackageReference Include="Meziantou.Analyzer" Version="2.0.219">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.9" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -108,7 +108,7 @@ public class Startup
{ {
builder.MigrationsHistoryTable("_efmigrationshistory", "public"); builder.MigrationsHistoryTable("_efmigrationshistory", "public");
}).UseSnakeCaseNamingConvention(); }).UseSnakeCaseNamingConvention();
options.EnableThreadSafetyChecks(false); options.EnableThreadSafetyChecks();
}, mareConfig.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024)); }, mareConfig.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024));
var signalRServiceBuilder = services.AddSignalR(hubOptions => var signalRServiceBuilder = services.AddSignalR(hubOptions =>
@@ -142,19 +142,22 @@ public class Startup
var options = ConfigurationOptions.Parse(redisConnection); var options = ConfigurationOptions.Parse(redisConnection);
var endpoint = options.EndPoints[0]; var hosts = options.EndPoints
string address = ""; .Select(ep =>
int port = 0; {
if (endpoint is DnsEndPoint dnsEndPoint) { address = dnsEndPoint.Host; port = dnsEndPoint.Port; } if (ep is DnsEndPoint dns) return (host: dns.Host, port: dns.Port);
if (endpoint is IPEndPoint ipEndPoint) { address = ipEndPoint.Address.ToString(); port = ipEndPoint.Port; } if (ep is IPEndPoint ip) return (host: ip.Address.ToString(), port: ip.Port);
return (host: (string?)null, port: 0);
})
.Where(x => x.host != null)
.Distinct()
.Select(x => new RedisHost { Host = x.host!, Port = x.port })
.ToArray();
var redisConfiguration = new RedisConfiguration() var redisConfiguration = new RedisConfiguration()
{ {
AbortOnConnectFail = true, AbortOnConnectFail = false,
KeyPrefix = "", KeyPrefix = "",
Hosts = new RedisHost[] Hosts = hosts,
{
new RedisHost(){ Host = address, Port = port },
},
AllowAdmin = true, AllowAdmin = true,
ConnectTimeout = options.ConnectTimeout, ConnectTimeout = options.ConnectTimeout,
Database = 0, Database = 0,
@@ -164,11 +167,11 @@ public class Startup
{ {
Mode = ServerEnumerationStrategy.ModeOptions.All, Mode = ServerEnumerationStrategy.ModeOptions.All,
TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any, TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any,
UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.Throw, UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.IgnoreIfOtherAvailable,
}, },
MaxValueLength = 1024, MaxValueLength = 1024,
PoolSize = mareConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50), PoolSize = mareConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50),
SyncTimeout = options.SyncTimeout, SyncTimeout = Math.Max(options.SyncTimeout, 10000),
}; };
services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(redisConfiguration); services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(redisConfiguration);