From 479b80a5a02f0d58901a6b5ecca20971ed6c3de4 Mon Sep 17 00:00:00 2001 From: azyges <229218900+azyges@users.noreply.github.com> Date: Wed, 8 Oct 2025 08:40:56 +0900 Subject: [PATCH] lightfinder changes: - removed all ability to provide your cid to the server through params, cid is gained from JWT claims - improved verification of who owns a cid, which includes locking a cid to a uid - locks and persisting entries of broadcasting are cleaned up on disconnection - method identification logic was rewritten to fit these changes --- LightlessAPI | 2 +- .../Configuration/BroadcastConfiguration.cs | 11 ++ .../Configuration/BroadcastOptions.cs | 6 + .../Configuration/IBroadcastConfiguration.cs | 4 + .../Hubs/LightlessHub.Functions.cs | 66 +++++++- .../Hubs/LightlessHub.Groups.cs | 81 +++++----- .../Hubs/LightlessHub.User.cs | 151 +++++++++++++----- .../LightlessSyncServer/Hubs/LightlessHub.cs | 5 + .../LightlessSyncServer/appsettings.json | 4 +- 9 files changed, 243 insertions(+), 87 deletions(-) diff --git a/LightlessAPI b/LightlessAPI index 167508d..f3c6064 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 167508d27b754347554797fa769c5feb3f91552e +Subproject commit f3c60648921abab03c3a6cc6142543f06ba02c45 diff --git a/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs index 893ec65..d2a0549 100644 --- a/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs +++ b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastConfiguration.cs @@ -59,6 +59,14 @@ public class BroadcastConfiguration : IBroadcastConfiguration return string.Concat(RedisKeyPrefix, hashedCid); } + public string BuildUserOwnershipKey(string userUid) + { + if (string.IsNullOrWhiteSpace(userUid)) + throw new ArgumentException("User UID must not be null or empty.", nameof(userUid)); + + return string.Concat(RedisKeyPrefix, "owner:", userUid); + } + public string BuildPairRequestNotification() { var template = Options.PairRequestNotificationTemplate; @@ -69,4 +77,7 @@ public class BroadcastConfiguration : IBroadcastConfiguration return template; } + + public int PairRequestRateLimit => Options.PairRequestRateLimit > 0 ? Options.PairRequestRateLimit : 5; + public int PairRequestRateWindow => Options.PairRequestRateWindow > 0 ? Options.PairRequestRateWindow : 60; } diff --git a/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastOptions.cs b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastOptions.cs index 858255a..ecfaf3a 100644 --- a/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastOptions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Configuration/BroadcastOptions.cs @@ -20,4 +20,10 @@ public class BroadcastOptions public bool EnableSyncshellBroadcastPayloads { get; set; } = true; public string PairRequestNotificationTemplate { get; set; } = "{DisplayName} sent you a pair request. To accept, right-click them, open the context menu, and send a request back."; + + [Range(1, int.MaxValue)] + public int PairRequestRateLimit { get; set; } = 5; + + [Range(1, int.MaxValue)] + public int PairRequestRateWindow { get; set; } = 60; } diff --git a/LightlessSyncServer/LightlessSyncServer/Configuration/IBroadcastConfiguration.cs b/LightlessSyncServer/LightlessSyncServer/Configuration/IBroadcastConfiguration.cs index 0f741f7..3a38aa2 100644 --- a/LightlessSyncServer/LightlessSyncServer/Configuration/IBroadcastConfiguration.cs +++ b/LightlessSyncServer/LightlessSyncServer/Configuration/IBroadcastConfiguration.cs @@ -12,5 +12,9 @@ public interface IBroadcastConfiguration bool EnableSyncshellBroadcastPayloads { get; } string BuildRedisKey(string hashedCid); + string BuildUserOwnershipKey(string userUid); string BuildPairRequestNotification(); + + int PairRequestRateLimit { get; } + int PairRequestRateWindow { get; } } diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs index 40ca003..b7b816d 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Functions.cs @@ -1,11 +1,13 @@ -using LightlessSyncShared.Models; -using Microsoft.EntityFrameworkCore; -using LightlessSyncServer.Utils; -using LightlessSyncShared.Utils; using LightlessSync.API.Data; using LightlessSync.API.Dto.Group; +using LightlessSyncServer.Utils; using LightlessSyncShared.Metrics; +using LightlessSyncShared.Models; +using LightlessSyncShared.Utils; using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using StackExchange.Redis; +using System.Text.Json; using System.Threading; namespace LightlessSyncServer.Hubs; @@ -95,13 +97,18 @@ public partial class LightlessHub private async Task RemoveUserFromRedis() { + if (IsValidHashedCid(UserCharaIdent)) + { + await _redis.RemoveAsync("CID:" + UserCharaIdent, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); + } + await _redis.RemoveAsync("UID:" + UserUID, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); } private async Task EnsureUserHasVanity(string uid, CancellationToken cancellationToken = default) { cancellationToken = cancellationToken == default && _contextAccessor.HttpContext != null - ? _contextAccessor.HttpContext.RequestAborted + ? RequestAbortedToken : cancellationToken; var user = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid, cancellationToken).ConfigureAwait(false); @@ -120,6 +127,47 @@ public partial class LightlessHub return user; } + private async Task ClearOwnedBroadcastLock() + { + var db = _redis.Database; + var ownershipKey = _broadcastConfiguration.BuildUserOwnershipKey(UserUID); + var ownedCidValue = await db.StringGetAsync(ownershipKey).ConfigureAwait(false); + if (ownedCidValue.IsNullOrEmpty) + return; + + var ownedCid = ownedCidValue.ToString(); + + await db.KeyDeleteAsync(ownershipKey, CommandFlags.FireAndForget).ConfigureAwait(false); + + if (string.IsNullOrEmpty(ownedCid)) + return; + + var broadcastKey = _broadcastConfiguration.BuildRedisKey(ownedCid); + var broadcastValue = await db.StringGetAsync(broadcastKey).ConfigureAwait(false); + if (broadcastValue.IsNullOrEmpty) + return; + + BroadcastRedisEntry? entry; + try + { + entry = JsonSerializer.Deserialize(broadcastValue!); + } + catch (Exception ex) + { + _logger.LogCallWarning(LightlessHubLogger.Args("failed to deserialize broadcast during disconnect cleanup", "CID", ownedCid, "Value", broadcastValue, "Error", ex)); + return; + } + + if (entry is null) + return; + + if (entry.HasOwner() && !entry.OwnedBy(UserUID)) + return; + + await db.KeyDeleteAsync(broadcastKey, CommandFlags.FireAndForget).ConfigureAwait(false); + _logger.LogCallInfo(LightlessHubLogger.Args("broadcast cleaned on disconnect", UserUID, "CID", entry.HashedCID, "GID", entry.GID)); + } + private async Task SendGroupDeletedToAll(List groupUsers) { foreach (var pair in groupUsers) @@ -188,7 +236,13 @@ public partial class LightlessHub private async Task UpdateUserOnRedis() { - await _redis.AddAsync("UID:" + UserUID, UserCharaIdent, TimeSpan.FromSeconds(60), StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); + var hashedCid = UserCharaIdent; + if (IsValidHashedCid(hashedCid)) + { + await _redis.AddAsync("CID:" + hashedCid, UserUID, TimeSpan.FromSeconds(60), StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); + } + + await _redis.AddAsync("UID:" + UserUID, hashedCid, TimeSpan.FromSeconds(60), StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false); } private async Task UserGroupLeave(GroupPair groupUserPair, string userIdent, Dictionary allUserPairs, string? uid = null) diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs index 69a32e4..30cb142 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.Groups.cs @@ -59,7 +59,7 @@ public partial class LightlessHub group.PreferDisableAnimations = dto.Permissions.HasFlag(GroupPermissions.PreferDisableAnimations); group.PreferDisableVFX = dto.Permissions.HasFlag(GroupPermissions.PreferDisableVFX); - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToList(); await Clients.Users(groupPairs).Client_GroupChangePermissions(new GroupPermissionDto(dto.Group, dto.Permissions)).ConfigureAwait(false); @@ -137,7 +137,7 @@ public partial class LightlessHub var allUserPairs = await GetAllPairInfo(pair.GroupUserUID).ConfigureAwait(false); - var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false); DbContext.CharaDataAllowances.RemoveRange(sharedData); foreach (var groupUserPair in groupPairs.Where(p => !string.Equals(p.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal))) @@ -181,7 +181,7 @@ public partial class LightlessHub var allUserPairs = await GetAllPairInfo(pair.GroupUserUID).ConfigureAwait(false); - var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false); DbContext.CharaDataAllowances.RemoveRange(sharedData); @@ -199,15 +199,15 @@ public partial class LightlessHub public async Task GroupCreate() { _logger.LogCallInfo(); - var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var existingJoinedGroups = await DbContext.GroupPairs.CountAsync(u => u.GroupUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); + var existingJoinedGroups = await DbContext.GroupPairs.CountAsync(u => u.GroupUserUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (existingGroupsByUser >= _maxExistingGroupsByUser || existingJoinedGroups >= _maxJoinedGroupsByUser) { throw new System.Exception($"Max groups for user is {_maxExistingGroupsByUser}, max joined groups is {_maxJoinedGroupsByUser}."); } var gid = StringUtils.GenerateRandomString(12); - while (await DbContext.Groups.AnyAsync(g => g.GID == "LLS-" + gid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false)) + while (await DbContext.Groups.AnyAsync(g => g.GID == "LLS-" + gid, cancellationToken: RequestAbortedToken).ConfigureAwait(false)) { gid = StringUtils.GenerateRandomString(12); } @@ -218,7 +218,7 @@ public partial class LightlessHub var hashedPw = StringUtils.Sha256String(passwd); var currentTime = DateTime.UtcNow; - UserDefaultPreferredPermission defaultPermissions = await DbContext.UserDefaultPreferredPermissions.SingleAsync(u => u.UserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + UserDefaultPreferredPermission defaultPermissions = await DbContext.UserDefaultPreferredPermissions.SingleAsync(u => u.UserUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); Group newGroup = new() { @@ -250,12 +250,12 @@ public partial class LightlessHub DisableVFX = defaultPermissions.DisableGroupAnimations, }; - await DbContext.Groups.AddAsync(newGroup, _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - await DbContext.GroupPairs.AddAsync(initialPair, _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - await DbContext.GroupPairPreferredPermissions.AddAsync(initialPrefPermissions, _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.Groups.AddAsync(newGroup, RequestAbortedToken).ConfigureAwait(false); + await DbContext.GroupPairs.AddAsync(initialPair, RequestAbortedToken).ConfigureAwait(false); + await DbContext.GroupPairPreferredPermissions.AddAsync(initialPrefPermissions, RequestAbortedToken).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); - var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(), newGroup.ToEnum(), initialPrefPermissions.ToEnum(), initialPair.ToEnum(), new(StringComparer.Ordinal))) @@ -314,10 +314,10 @@ public partial class LightlessHub _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); - var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false); DbContext.RemoveRange(groupPairs); DbContext.Remove(group); - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); await Clients.Users(groupPairs.Select(g => g.GroupUserUID)).Client_GroupDelete(new GroupDto(group.ToGroupData())).ConfigureAwait(false); @@ -332,7 +332,7 @@ public partial class LightlessHub var (userHasRights, group) = await TryValidateGroupModeratorOrOwner(dto.GID).ConfigureAwait(false); if (!userHasRights) return []; - var banEntries = await DbContext.GroupBans.Include(b => b.BannedUser).Where(g => g.GroupGID == dto.Group.GID).AsNoTracking().ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var banEntries = await DbContext.GroupBans.Include(b => b.BannedUser).Where(g => g.GroupGID == dto.Group.GID).AsNoTracking().ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false); List bannedGroupUsers = banEntries.Select(b => new BannedGroupUserDto(group.ToGroupData(), b.BannedUser.ToUserData(), b.BannedReason, b.BannedOn, @@ -350,14 +350,14 @@ public partial class LightlessHub _logger.LogCallInfo(LightlessHubLogger.Args(dto)); - var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid, cancellationToken: RequestAbortedToken).ConfigureAwait(false); var groupGid = group?.GID ?? string.Empty; - var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); var hashedPw = StringUtils.Sha256String(dto.Password); - var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); - var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid, cancellationToken: RequestAbortedToken).ConfigureAwait(false); + var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); + var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); + var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (group == null || (!string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal) && oneTimeInvite == null) @@ -378,7 +378,7 @@ public partial class LightlessHub _logger.LogCallInfo(LightlessHubLogger.Args(dto)); - var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid, cancellationToken: RequestAbortedToken).ConfigureAwait(false); var groupGid = group?.GID ?? string.Empty; var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false); var isHashedPassword = dto.Password.Length == 64 && dto.Password.All(Uri.IsHexDigit); @@ -416,7 +416,7 @@ public partial class LightlessHub FromFinder = isHashedPassword }; - var preferredPermissions = await DbContext.GroupPairPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.GroupGID == group.GID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var preferredPermissions = await DbContext.GroupPairPreferredPermissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.GroupGID == group.GID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (preferredPermissions == null) { GroupPairPreferredPermission newPerms = new() @@ -441,13 +441,13 @@ public partial class LightlessHub DbContext.Update(preferredPermissions); } - await DbContext.GroupPairs.AddAsync(newPair, _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.GroupPairs.AddAsync(newPair, RequestAbortedToken).ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args(aliasOrGid, "Success")); - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); - var groupInfos = await DbContext.GroupPairs.Where(u => u.GroupGID == group.GID && (u.IsPinned || u.IsModerator)).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var groupInfos = await DbContext.GroupPairs.Where(u => u.GroupGID == group.GID && (u.IsPinned || u.IsModerator)).ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false); await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.ToEnum(), preferredPermissions.ToEnum(), newPair.ToEnum(), groupInfos.ToDictionary(u => u.GroupUserUID, u => u.ToEnum(), StringComparer.Ordinal))).ConfigureAwait(false); @@ -575,7 +575,7 @@ public partial class LightlessHub } } - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); return true; } @@ -693,7 +693,7 @@ public partial class LightlessHub .Client_GroupPairLeft(new GroupPairDto(dto.Group, pair.GroupUser.ToUserData())).ConfigureAwait(false); } - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); return usersToPrune.Count(); } @@ -717,15 +717,15 @@ public partial class LightlessHub var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == group.GID).AsNoTracking().ToList(); await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).Client_GroupPairLeft(dto).ConfigureAwait(false); - var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == dto.UID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == dto.UID).ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false); DbContext.CharaDataAllowances.RemoveRange(sharedData); - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); var userIdent = await GetUserIdent(dto.User.UID).ConfigureAwait(false); if (userIdent == null) { - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); return; } @@ -743,7 +743,7 @@ public partial class LightlessHub { _logger.LogCallInfo(LightlessHubLogger.Args(dto)); - var cancellationToken = _contextAccessor.HttpContext.RequestAborted; + var cancellationToken = RequestAbortedToken; var data = await DbContext.GroupProfiles .FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID, cancellationToken) @@ -780,7 +780,7 @@ public partial class LightlessHub var groupProfileDb = await DbContext.GroupProfiles .FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID, - _contextAccessor.HttpContext.RequestAborted) + RequestAbortedToken) .ConfigureAwait(false); if (groupProfileDb != null) @@ -800,11 +800,11 @@ public partial class LightlessHub }; await DbContext.GroupProfiles.AddAsync(groupProfile, - _contextAccessor.HttpContext.RequestAborted) + RequestAbortedToken) .ConfigureAwait(false); } - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); } [Authorize(Policy = "Identified")] @@ -836,9 +836,9 @@ public partial class LightlessHub userPair.IsModerator = false; } - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); - var groupPairs = await DbContext.GroupPairs.AsNoTracking().Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToListAsync(cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var groupPairs = await DbContext.GroupPairs.AsNoTracking().Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToListAsync(cancellationToken: RequestAbortedToken).ConfigureAwait(false); await Clients.Users(groupPairs).Client_GroupPairChangeUserInfo(new GroupPairUserInfoDto(dto.Group, dto.User, userPair.ToEnum())).ConfigureAwait(false); } @@ -847,7 +847,7 @@ public partial class LightlessHub { _logger.LogCallInfo(); - var ct = _contextAccessor.HttpContext.RequestAborted; + var ct = RequestAbortedToken; var result = await ( from gp in DbContext.GroupPairs @@ -899,11 +899,11 @@ public partial class LightlessHub var (userHasRights, _) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false); if (!userHasRights) return; - var banEntry = await DbContext.GroupBans.SingleOrDefaultAsync(g => g.GroupGID == dto.Group.GID && g.BannedUserUID == dto.User.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var banEntry = await DbContext.GroupBans.SingleOrDefaultAsync(g => g.GroupGID == dto.Group.GID && g.BannedUserUID == dto.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (banEntry == null) return; DbContext.Remove(banEntry); - await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); } @@ -991,5 +991,4 @@ public partial class LightlessHub return results; } - } \ No newline at end of file diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs index d9ba6eb..ecd7025 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.User.cs @@ -1,4 +1,4 @@ -using LightlessSync.API.Data; +using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; @@ -35,7 +35,7 @@ public partial class LightlessHub if (string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal) || string.IsNullOrWhiteSpace(dto.User.UID)) return; // grab other user, check if it exists and if a pair already exists - var otherUser = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var otherUser = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (otherUser == null) { await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"Cannot pair with {dto.User.UID}, UID does not exist").ConfigureAwait(false); @@ -51,7 +51,7 @@ public partial class LightlessHub var existingEntry = await DbContext.ClientPairs.AsNoTracking() .FirstOrDefaultAsync(p => - p.User.UID == UserUID && p.OtherUserUID == otherUser.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + p.User.UID == UserUID && p.OtherUserUID == otherUser.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (existingEntry != null) { @@ -60,7 +60,7 @@ public partial class LightlessHub } // grab self create new client pair and save - var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success")); @@ -76,7 +76,7 @@ public partial class LightlessHub var permissions = existingData?.OwnPermissions; if (permissions == null || !permissions.Sticky) { - var ownDefaultPermissions = await DbContext.UserDefaultPreferredPermissions.AsNoTracking().SingleOrDefaultAsync(f => f.UserUID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var ownDefaultPermissions = await DbContext.UserDefaultPreferredPermissions.AsNoTracking().SingleOrDefaultAsync(f => f.UserUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); permissions = new UserPermissionSet() { @@ -89,7 +89,7 @@ public partial class LightlessHub Sticky = true }; - var existingDbPerms = await DbContext.Permissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.OtherUserUID == otherUser.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingDbPerms = await DbContext.Permissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.OtherUserUID == otherUser.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (existingDbPerms == null) { await DbContext.Permissions.AddAsync(permissions).ConfigureAwait(false); @@ -143,17 +143,15 @@ public partial class LightlessHub } [Authorize(Policy = "Identified")] - public async Task TryPairWithContentId(string otherCid, string myCid) + public async Task TryPairWithContentId(string otherCid) { + var myCid = UserCharaIdent; + if (string.IsNullOrWhiteSpace(otherCid) || string.IsNullOrWhiteSpace(myCid)) return; - bool IsValidCid(string cid) => cid.Length == 64 && cid.All(Uri.IsHexDigit) && !cid.All(c => c == '0'); - - if (!IsValidCid(myCid) || !IsValidCid(otherCid)) - { + if (!IsValidHashedCid(myCid) || !IsValidHashedCid(otherCid)) return; - } if (string.Equals(otherCid, myCid, StringComparison.Ordinal)) { @@ -181,12 +179,33 @@ public partial class LightlessHub try { var payload = JsonSerializer.Deserialize(json); - if (payload?.UID == null) + if (payload?.UID == null || string.IsNullOrWhiteSpace(payload.HashedCid)) { _logger.LogCallWarning(LightlessHubLogger.Args("invalid payload", reverseKey)); return; } + if (!IsValidHashedCid(payload.HashedCid) || !string.Equals(payload.HashedCid, otherCid, StringComparison.Ordinal)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("pairing cid mismatch", reverseKey, payload.HashedCid, otherCid)); + return; + } + + var expectedRequesterUid = await _redis.GetAsync("CID:" + payload.HashedCid).ConfigureAwait(false); + if (!string.Equals(expectedRequesterUid, payload.UID, StringComparison.Ordinal)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("pairing uid mismatch", reverseKey, payload.HashedCid, payload.UID, expectedRequesterUid ?? "null")); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "Pair request could not be validated.").ConfigureAwait(false); + return; + } + + if (payload.Timestamp == default || DateTime.UtcNow - payload.Timestamp > TimeSpan.FromMinutes(5)) + { + _logger.LogCallWarning(LightlessHubLogger.Args("stale pairing payload", reverseKey, payload.Timestamp)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "Pair request expired.").ConfigureAwait(false); + return; + } + var sender = await _pairService.TryAddPairAsync(UserUID, payload.UID); var receiver = await _pairService.TryAddPairAsync(payload.UID, UserUID); @@ -251,9 +270,30 @@ public partial class LightlessHub } else { + int maxRequests = _broadcastConfiguration.PairRequestRateLimit; + int requestWindow = _broadcastConfiguration.PairRequestRateWindow; + TimeSpan window = TimeSpan.FromSeconds(requestWindow); + var rateKey = $"pairing:limit:{UserUID}"; + var db = _redis.Database; + + var count = (long)await db.StringIncrementAsync(rateKey).ConfigureAwait(false); + if (count == 1) + { + await db.KeyExpireAsync(rateKey, window).ConfigureAwait(false); + } + + if (count > maxRequests) + { + var ttl = await db.KeyTimeToLiveAsync(rateKey).ConfigureAwait(false); + var secondsLeft = ttl?.TotalSeconds > 0 ? (int)ttl.Value.TotalSeconds : requestWindow; + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"You have exceeded the pair request limit. Please wait {secondsLeft} seconds before trying again.").ConfigureAwait(false); + return; + } + var payload = new PairingPayload { UID = UserUID, + HashedCid = myCid, Timestamp = DateTime.UtcNow }; @@ -262,14 +302,16 @@ public partial class LightlessHub await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, $"Pair request sent. Waiting for the other player to confirm.").ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args("stored pairing request", myCid, otherCid)); - await NotifyBroadcastOwnerOfPairRequest(myCid, otherCid).ConfigureAwait(false); + await NotifyBroadcastOwnerOfPairRequest(otherCid).ConfigureAwait(false); } } - private async Task NotifyBroadcastOwnerOfPairRequest(string myHashedCid, string targetHashedCid) + private async Task NotifyBroadcastOwnerOfPairRequest(string targetHashedCid) { - if (string.IsNullOrWhiteSpace(targetHashedCid) || string.IsNullOrWhiteSpace(myHashedCid)) + var myHashedCid = UserCharaIdent; + + if (!IsValidHashedCid(targetHashedCid) || !IsValidHashedCid(myHashedCid)) return; if (!_broadcastConfiguration.EnableBroadcasting || !_broadcastConfiguration.NotifyOwnerOnPairRequest) @@ -323,6 +365,7 @@ public partial class LightlessHub private class PairingPayload { public string UID { get; set; } = string.Empty; + public string HashedCid { get; set; } = string.Empty; public DateTime Timestamp { get; set; } } @@ -338,8 +381,10 @@ public partial class LightlessHub } [Authorize(Policy = "Identified")] - public async Task SetBroadcastStatus(string hashedCid, bool enabled, GroupBroadcastRequestDto? groupDto = null) + public async Task SetBroadcastStatus(bool enabled, GroupBroadcastRequestDto? groupDto = null) { + var hashedCid = UserCharaIdent; + if (enabled && !_broadcastConfiguration.EnableBroadcasting) { _logger.LogCallWarning(LightlessHubLogger.Args("broadcast disabled", UserUID, "CID", hashedCid)); @@ -347,9 +392,9 @@ public partial class LightlessHub return; } - if (string.IsNullOrWhiteSpace(hashedCid) || hashedCid.Length != 64 || !hashedCid.All(c => Uri.IsHexDigit(c))) + if (!IsValidHashedCid(hashedCid)) { - _logger.LogCallWarning(LightlessHubLogger.Args("invalid cid format", UserUID, "CID", hashedCid)); + _logger.LogCallWarning(LightlessHubLogger.Args("invalid cid format for user ident", UserUID, "CID", hashedCid)); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Invalid CID format.").ConfigureAwait(false); return; } @@ -361,11 +406,32 @@ public partial class LightlessHub var db = _redis.Database; var broadcastKey = _broadcastConfiguration.BuildRedisKey(hashedCid); + var ownershipKey = _broadcastConfiguration.BuildUserOwnershipKey(UserUID); + var ownedCidValue = await db.StringGetAsync(ownershipKey).ConfigureAwait(false); + var ownedCid = ownedCidValue.IsNullOrEmpty ? null : ownedCidValue.ToString(); if (enabled) { string? gid = null; + if (!string.IsNullOrEmpty(ownedCid) && !string.Equals(ownedCid, hashedCid, StringComparison.Ordinal)) + { + var ownedBroadcastKey = _broadcastConfiguration.BuildRedisKey(ownedCid); + var ownedBroadcastValue = await db.StringGetAsync(ownedBroadcastKey).ConfigureAwait(false); + + if (ownedBroadcastValue.IsNullOrEmpty) + { + await db.KeyDeleteAsync(ownershipKey, CommandFlags.FireAndForget).ConfigureAwait(false); + ownedCid = null; + } + else + { + _logger.LogCallWarning(LightlessHubLogger.Args("multiple broadcast lock attempt", UserUID, "ExistingCID", ownedCid, "AttemptedCID", hashedCid)); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You already have an active Lightfinder lock. Disable it before enabling another.").ConfigureAwait(false); + return; + } + } + if (groupDto is not null) { if (!_broadcastConfiguration.EnableSyncshellBroadcastPayloads) @@ -400,7 +466,7 @@ public partial class LightlessHub if (existingEntry is not null && existingEntry.HasOwner() && !existingEntry.OwnedBy(UserUID)) { _logger.LogCallWarning(LightlessHubLogger.Args("unauthorized attempt to take broadcast ownership", UserUID, "CID", hashedCid, "ExistingOwner", existingEntry.OwnerUID)); - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Another user is already broadcasting with that CID."); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Another user is already broadcasting with that CID.").ConfigureAwait(false); return; } } @@ -414,6 +480,7 @@ public partial class LightlessHub var json = JsonSerializer.Serialize(entry); await db.StringSetAsync(broadcastKey, json, _broadcastConfiguration.BroadcastEntryTtl).ConfigureAwait(false); + await db.StringSetAsync(ownershipKey, hashedCid, _broadcastConfiguration.BroadcastEntryTtl).ConfigureAwait(false); _logger.LogCallInfo(LightlessHubLogger.Args("broadcast enabled", hashedCid, "GID", gid)); } else @@ -436,22 +503,35 @@ public partial class LightlessHub if (entry is null || !string.Equals(entry.HashedCID, hashedCid, StringComparison.Ordinal)) { _logger.LogCallWarning(LightlessHubLogger.Args("unauthorized attempt to remove broadcast", UserUID, "CID", hashedCid, "Stored", entry?.HashedCID)); - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You can only disable your own broadcast. :3"); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You can only disable your own broadcast. :3").ConfigureAwait(false); return; } if (entry.HasOwner() && !entry.OwnedBy(UserUID)) { _logger.LogCallWarning(LightlessHubLogger.Args("unauthorized attempt to remove broadcast", UserUID, "CID", hashedCid, "Owner", entry.OwnerUID)); - await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You can only disable your own broadcast. :3"); + await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You can only disable your own broadcast. :3").ConfigureAwait(false); return; } await db.KeyDeleteAsync(broadcastKey).ConfigureAwait(false); + + if (!string.IsNullOrEmpty(ownedCid) && string.Equals(ownedCid, hashedCid, StringComparison.Ordinal)) + { + await db.KeyDeleteAsync(ownershipKey).ConfigureAwait(false); + } + _logger.LogCallInfo(LightlessHubLogger.Args("broadcast disabled", hashedCid, "GID", entry.GID)); } } + private static bool IsValidHashedCid(string? cid) + { + if (string.IsNullOrWhiteSpace(cid)) + return false; + + return cid.Length == 64 && cid.All(Uri.IsHexDigit) && !cid.All(c => c == '0'); + } [Authorize(Policy = "Identified")] public async Task IsUserBroadcasting(string hashedCid) @@ -459,18 +539,13 @@ public partial class LightlessHub if (!_broadcastConfiguration.EnableBroadcasting) return null; - if (string.IsNullOrWhiteSpace(hashedCid) || hashedCid.Length != 64 || !hashedCid.All(c => Uri.IsHexDigit(c))) + if (!IsValidHashedCid(hashedCid)) { _logger.LogCallWarning(LightlessHubLogger.Args("invalid cid format", UserUID, "CID", hashedCid)); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Invalid CID format.").ConfigureAwait(false); return null; } - if (hashedCid.All(c => c == '0')) - { - return null; - } - var db = _redis.Database; var key = _broadcastConfiguration.BuildRedisKey(hashedCid); @@ -507,14 +582,16 @@ public partial class LightlessHub } [Authorize(Policy = "Identified")] - public async Task GetBroadcastTtl(string hashedCid) + public async Task GetBroadcastTtl() { if (!_broadcastConfiguration.EnableBroadcasting) return null; - if (string.IsNullOrWhiteSpace(hashedCid) || hashedCid.Length != 64 || !hashedCid.All(c => Uri.IsHexDigit(c))) + var hashedCid = UserCharaIdent; + + if (!IsValidHashedCid(hashedCid)) { - _logger.LogCallWarning(LightlessHubLogger.Args("invalid cid format", UserUID, "CID", hashedCid)); + _logger.LogCallWarning(LightlessHubLogger.Args("invalid cid format for user ident", UserUID, "CID", hashedCid)); await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Invalid CID format.").ConfigureAwait(false); return null; } @@ -587,9 +664,7 @@ public partial class LightlessHub var tasks = new Dictionary>(hashedCids.Count); foreach (var cid in hashedCids) { - bool validHash = !string.IsNullOrWhiteSpace(cid) && cid.Length == 64 && cid.All(Uri.IsHexDigit) && !cid.All(c => c == '0'); - - if (!validHash) + if (!IsValidHashedCid(cid)) { tasks[cid] = Task.FromResult(new RedisValueWithExpiry(RedisValue.Null, null)); continue; @@ -657,7 +732,7 @@ public partial class LightlessHub { _logger.LogCallInfo(); - var userEntry = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var userEntry = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); var secondaryUsers = await DbContext.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == UserUID).Select(c => c.User).ToListAsync().ConfigureAwait(false); foreach (var user in secondaryUsers) { @@ -710,7 +785,7 @@ public partial class LightlessHub return new UserProfileDto(user.User, false, null, null, "Due to the pause status you cannot access this users profile."); } - var data = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.User.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var data = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (data == null) return new UserProfileDto(user.User, false, null, null, null); if (data.FlaggedForReport) return new UserProfileDto(user.User, true, null, null, "This profile is flagged for report and pending evaluation"); @@ -867,7 +942,7 @@ public partial class LightlessHub // check if client pair even exists ClientPair callerPair = - await DbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == dto.User.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + await DbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == dto.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (callerPair == null) return; var pairData = await GetPairInfo(UserUID, dto.User.UID).ConfigureAwait(false); @@ -918,7 +993,7 @@ public partial class LightlessHub if (!string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) throw new HubException("Cannot modify profile data for anyone but yourself"); - var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false); + var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false); if (existingData?.FlaggedForReport ?? false) { diff --git a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs index 402e8be..318fc99 100644 --- a/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs +++ b/LightlessSyncServer/LightlessSyncServer/Hubs/LightlessHub.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using StackExchange.Redis.Extensions.Core.Abstractions; using System.Collections.Concurrent; +using System.Threading; namespace LightlessSyncServer.Hubs; @@ -44,6 +45,8 @@ public partial class LightlessHub : Hub, ILightlessHub private readonly int _maxCharaDataByUser; private readonly int _maxCharaDataByUserVanity; + private CancellationToken RequestAbortedToken => _contextAccessor.HttpContext?.RequestAborted ?? Context?.ConnectionAborted ?? CancellationToken.None; + public LightlessHub(LightlessMetrics lightlessMetrics, IDbContextFactory lightlessDbContextFactory, ILogger logger, SystemInfoService systemInfoService, IConfigurationService configuration, IHttpContextAccessor contextAccessor, @@ -194,6 +197,8 @@ public partial class LightlessHub : Hub, ILightlessHub if (exception != null) _logger.LogCallWarning(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, exception.Message, exception.StackTrace)); + await ClearOwnedBroadcastLock().ConfigureAwait(false); + await RemoveUserFromRedis().ConfigureAwait(false); _lightlessCensus.ClearStatistics(UserUID); diff --git a/LightlessSyncServer/LightlessSyncServer/appsettings.json b/LightlessSyncServer/LightlessSyncServer/appsettings.json index 80524cb..991b30a 100644 --- a/LightlessSyncServer/LightlessSyncServer/appsettings.json +++ b/LightlessSyncServer/LightlessSyncServer/appsettings.json @@ -36,7 +36,9 @@ "NotifyOwnerOnPairRequest": true, "EnableBroadcasting": true, "EnableSyncshellBroadcastPayloads": true, - "PairRequestNotificationTemplate": "{DisplayName} sent you a pair request. To accept, right-click them, open the context menu, and send a request back." + "PairRequestNotificationTemplate": "{DisplayName} sent you a pair request. To accept, right-click them, open the context menu, and send a request back.", + "PairRequestRateLimit": 5, + "PairRequestRateWindow": 60 }, "AllowedHosts": "*", "Kestrel": {