Compare commits
10 Commits
removal-ca
...
cdn-downlo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49177e639e | ||
|
|
79483205f1 | ||
| aadfaca629 | |||
| 729d781fa3 | |||
|
|
be95f24dcd | ||
|
|
a1f9526c23 | ||
|
|
0450255d6d | ||
|
|
b6907a2704 | ||
|
|
479b80a5a0 | ||
| d4d6e21381 |
1
.gitmodules
vendored
1
.gitmodules
vendored
@@ -1,3 +1,4 @@
|
||||
[submodule "LightlessAPI"]
|
||||
path = LightlessAPI
|
||||
url = https://git.lightless-sync.org/Lightless-Sync/LightlessAPI
|
||||
branch = main
|
||||
|
||||
Submodule LightlessAPI updated: 167508d27b...44fbe10458
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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<User?> 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<BroadcastRedisEntry>(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<GroupPair> 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<string, UserInfo> allUserPairs, string? uid = null)
|
||||
|
||||
@@ -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().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().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<GroupJoinDto> GroupCreate()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == UserUID).ConfigureAwait(false);
|
||||
var existingJoinedGroups = await DbContext.GroupPairs.CountAsync(u => u.GroupUserUID == UserUID).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).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).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).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().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().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<BannedGroupUserDto> 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).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 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).ConfigureAwait(false);
|
||||
var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||
var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID).ConfigureAwait(false);
|
||||
var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw).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).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).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().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().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().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().ConfigureAwait(false);
|
||||
await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false);
|
||||
|
||||
var userIdent = await GetUserIdent(dto.User.UID).ConfigureAwait(false);
|
||||
if (userIdent == null)
|
||||
{
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -743,6 +743,8 @@ public partial class LightlessHub
|
||||
{
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||
|
||||
var cancellationToken = RequestAbortedToken;
|
||||
|
||||
var data = await DbContext.GroupProfiles
|
||||
.FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID)
|
||||
.ConfigureAwait(false);
|
||||
@@ -777,7 +779,8 @@ public partial class LightlessHub
|
||||
if (!hasRights) return;
|
||||
|
||||
var groupProfileDb = await DbContext.GroupProfiles
|
||||
.FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID)
|
||||
.FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID,
|
||||
RequestAbortedToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (groupProfileDb != null)
|
||||
@@ -797,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")]
|
||||
@@ -833,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().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);
|
||||
}
|
||||
|
||||
@@ -844,6 +847,8 @@ public partial class LightlessHub
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
|
||||
var ct = RequestAbortedToken;
|
||||
|
||||
var result = await (
|
||||
from gp in DbContext.GroupPairs
|
||||
.Include(gp => gp.Group)
|
||||
@@ -894,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).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"));
|
||||
}
|
||||
@@ -985,4 +990,6 @@ public partial class LightlessHub
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSyncServer.Utils;
|
||||
using LightlessSyncServer.Configuration;
|
||||
using LightlessSyncShared.Metrics;
|
||||
using LightlessSyncShared.Models;
|
||||
using LightlessSyncShared.Utils;
|
||||
@@ -34,7 +33,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).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);
|
||||
@@ -50,7 +49,7 @@ public partial class LightlessHub
|
||||
var existingEntry =
|
||||
await DbContext.ClientPairs.AsNoTracking()
|
||||
.FirstOrDefaultAsync(p =>
|
||||
p.User.UID == UserUID && p.OtherUserUID == otherUser.UID).ConfigureAwait(false);
|
||||
p.User.UID == UserUID && p.OtherUserUID == otherUser.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
|
||||
|
||||
if (existingEntry != null)
|
||||
{
|
||||
@@ -59,7 +58,7 @@ public partial class LightlessHub
|
||||
}
|
||||
|
||||
// grab self create new client pair and save
|
||||
var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
||||
var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID, cancellationToken: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
|
||||
|
||||
@@ -75,7 +74,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).ConfigureAwait(false);
|
||||
var ownDefaultPermissions = await DbContext.UserDefaultPreferredPermissions.AsNoTracking().SingleOrDefaultAsync(f => f.UserUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
|
||||
|
||||
permissions = new UserPermissionSet()
|
||||
{
|
||||
@@ -88,7 +87,7 @@ public partial class LightlessHub
|
||||
Sticky = true
|
||||
};
|
||||
|
||||
var existingDbPerms = await DbContext.Permissions.SingleOrDefaultAsync(u => u.UserUID == UserUID && u.OtherUserUID == otherUser.UID).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);
|
||||
@@ -142,17 +141,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))
|
||||
{
|
||||
@@ -180,12 +177,33 @@ public partial class LightlessHub
|
||||
try
|
||||
{
|
||||
var payload = JsonSerializer.Deserialize<PairingPayload>(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<string>("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);
|
||||
|
||||
@@ -250,9 +268,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
|
||||
};
|
||||
|
||||
@@ -261,14 +300,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)
|
||||
@@ -322,6 +363,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; }
|
||||
}
|
||||
|
||||
@@ -337,8 +379,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));
|
||||
@@ -346,9 +390,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;
|
||||
}
|
||||
@@ -360,11 +404,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)
|
||||
@@ -399,7 +464,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;
|
||||
}
|
||||
}
|
||||
@@ -413,6 +478,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
|
||||
@@ -435,22 +501,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<BroadcastStatusInfoDto?> IsUserBroadcasting(string hashedCid)
|
||||
@@ -458,18 +537,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);
|
||||
|
||||
@@ -506,14 +580,16 @@ public partial class LightlessHub
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<TimeSpan?> GetBroadcastTtl(string hashedCid)
|
||||
public async Task<TimeSpan?> 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;
|
||||
}
|
||||
@@ -586,9 +662,7 @@ public partial class LightlessHub
|
||||
var tasks = new Dictionary<string, Task<RedisValueWithExpiry>>(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;
|
||||
@@ -656,7 +730,7 @@ public partial class LightlessHub
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
|
||||
var userEntry = await DbContext.Users.SingleAsync(u => u.UID == UserUID).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)
|
||||
{
|
||||
@@ -709,7 +783,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).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");
|
||||
@@ -866,7 +940,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).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);
|
||||
@@ -917,7 +991,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).ConfigureAwait(false);
|
||||
var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
|
||||
|
||||
if (existingData?.FlaggedForReport ?? false)
|
||||
{
|
||||
|
||||
@@ -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>, ILightlessHub
|
||||
private readonly int _maxCharaDataByUser;
|
||||
private readonly int _maxCharaDataByUserVanity;
|
||||
|
||||
private CancellationToken RequestAbortedToken => _contextAccessor.HttpContext?.RequestAborted ?? Context?.ConnectionAborted ?? CancellationToken.None;
|
||||
|
||||
public LightlessHub(LightlessMetrics lightlessMetrics,
|
||||
IDbContextFactory<LightlessDbContext> lightlessDbContextFactory, ILogger<LightlessHub> logger, SystemInfoService systemInfoService,
|
||||
IConfigurationService<ServerConfiguration> configuration, IHttpContextAccessor contextAccessor,
|
||||
@@ -194,6 +197,8 @@ public partial class LightlessHub : Hub<ILightlessHub>, 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);
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -20,6 +20,8 @@ public class StaticFilesServerConfiguration : LightlessConfigurationBase
|
||||
public string ColdStorageDirectory { get; set; } = null;
|
||||
public double ColdStorageSizeHardLimitInGiB { get; set; } = -1;
|
||||
public int ColdStorageUnusedFileRetentionPeriodInDays { get; set; } = 30;
|
||||
public bool EnableDirectDownloads { get; set; } = true;
|
||||
public int DirectDownloadTokenLifetimeSeconds { get; set; } = 300;
|
||||
[RemoteConfiguration]
|
||||
public double SpeedTestHoursRateLimit { get; set; } = 0.5;
|
||||
[RemoteConfiguration]
|
||||
@@ -40,6 +42,8 @@ public class StaticFilesServerConfiguration : LightlessConfigurationBase
|
||||
sb.AppendLine($"{nameof(CacheDirectory)} => {CacheDirectory}");
|
||||
sb.AppendLine($"{nameof(DownloadQueueSize)} => {DownloadQueueSize}");
|
||||
sb.AppendLine($"{nameof(DownloadQueueReleaseSeconds)} => {DownloadQueueReleaseSeconds}");
|
||||
sb.AppendLine($"{nameof(EnableDirectDownloads)} => {EnableDirectDownloads}");
|
||||
sb.AppendLine($"{nameof(DirectDownloadTokenLifetimeSeconds)} => {DirectDownloadTokenLifetimeSeconds}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using LightlessSyncStaticFilesServer.Services;
|
||||
using LightlessSyncStaticFilesServer.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -32,12 +33,15 @@ public class ServerFilesController : ControllerBase
|
||||
private readonly IDbContextFactory<LightlessDbContext> _lightlessDbContext;
|
||||
private readonly LightlessMetrics _metricsClient;
|
||||
private readonly MainServerShardRegistrationService _shardRegistrationService;
|
||||
private readonly CDNDownloadUrlService _cdnDownloadUrlService;
|
||||
private readonly CDNDownloadsService _cdnDownloadsService;
|
||||
|
||||
public ServerFilesController(ILogger<ServerFilesController> logger, CachedFileProvider cachedFileProvider,
|
||||
IConfigurationService<StaticFilesServerConfiguration> configuration,
|
||||
IHubContext<LightlessHub> hubContext,
|
||||
IDbContextFactory<LightlessDbContext> lightlessDbContext, LightlessMetrics metricsClient,
|
||||
MainServerShardRegistrationService shardRegistrationService) : base(logger)
|
||||
MainServerShardRegistrationService shardRegistrationService, CDNDownloadUrlService cdnDownloadUrlService,
|
||||
CDNDownloadsService cdnDownloadsService) : base(logger)
|
||||
{
|
||||
_basePath = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false)
|
||||
? configuration.GetValue<string>(nameof(StaticFilesServerConfiguration.ColdStorageDirectory))
|
||||
@@ -48,6 +52,8 @@ public class ServerFilesController : ControllerBase
|
||||
_lightlessDbContext = lightlessDbContext;
|
||||
_metricsClient = metricsClient;
|
||||
_shardRegistrationService = shardRegistrationService;
|
||||
_cdnDownloadUrlService = cdnDownloadUrlService;
|
||||
_cdnDownloadsService = cdnDownloadsService;
|
||||
}
|
||||
|
||||
[HttpPost(LightlessFiles.ServerFiles_DeleteAll)]
|
||||
@@ -105,6 +111,16 @@ public class ServerFilesController : ControllerBase
|
||||
baseUrl = shard.Value ?? _configuration.GetValue<Uri>(nameof(StaticFilesServerConfiguration.CdnFullUrl));
|
||||
}
|
||||
|
||||
var cdnDownloadUrl = string.Empty;
|
||||
if (forbiddenFile == null)
|
||||
{
|
||||
var directUri = _cdnDownloadUrlService.TryCreateDirectDownloadUri(baseUrl, file.Hash);
|
||||
if (directUri != null)
|
||||
{
|
||||
cdnDownloadUrl = directUri.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
response.Add(new DownloadFileDto
|
||||
{
|
||||
FileExists = file.Size > 0,
|
||||
@@ -113,6 +129,7 @@ public class ServerFilesController : ControllerBase
|
||||
Hash = file.Hash,
|
||||
Size = file.Size,
|
||||
Url = baseUrl?.ToString() ?? string.Empty,
|
||||
CDNDownloadUrl = cdnDownloadUrl,
|
||||
RawSize = file.RawSize
|
||||
});
|
||||
}
|
||||
@@ -127,6 +144,22 @@ public class ServerFilesController : ControllerBase
|
||||
return Ok(JsonSerializer.Serialize(allFileShards.SelectMany(t => t.RegionUris.Select(v => v.Value.ToString()))));
|
||||
}
|
||||
|
||||
[HttpGet(LightlessFiles.ServerFiles_DirectDownload + "/{hash}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> DownloadFileDirect(string hash, [FromQuery] long expires, [FromQuery] string signature)
|
||||
{
|
||||
var result = await _cdnDownloadsService.GetDownloadAsync(hash, expires, signature).ConfigureAwait(false);
|
||||
|
||||
return result.Status switch
|
||||
{
|
||||
CDNDownloadsService.ResultStatus.Disabled => NotFound(),
|
||||
CDNDownloadsService.ResultStatus.Unauthorized => Unauthorized(),
|
||||
CDNDownloadsService.ResultStatus.NotFound => NotFound(),
|
||||
CDNDownloadsService.ResultStatus.Success => PhysicalFile(result.File!.FullName, "application/octet-stream"),
|
||||
_ => NotFound()
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost(LightlessFiles.ServerFiles_FilesSend)]
|
||||
public async Task<IActionResult> FilesSend([FromBody] FilesSendDto filesSendDto)
|
||||
{
|
||||
@@ -360,4 +393,4 @@ public class ServerFilesController : ControllerBase
|
||||
buffer[i] ^= 42;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using LightlessSync.API.Routes;
|
||||
using LightlessSyncStaticFilesServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace LightlessSyncStaticFilesServer.Controllers;
|
||||
|
||||
[Route(LightlessFiles.ServerFiles)]
|
||||
public class ShardServerFilesController : ControllerBase
|
||||
{
|
||||
private readonly CDNDownloadsService _cdnDownloadsService;
|
||||
|
||||
public ShardServerFilesController(ILogger<ShardServerFilesController> logger,
|
||||
CDNDownloadsService cdnDownloadsService) : base(logger)
|
||||
{
|
||||
_cdnDownloadsService = cdnDownloadsService;
|
||||
}
|
||||
|
||||
[HttpGet(LightlessFiles.ServerFiles_DirectDownload + "/{hash}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> DownloadFileDirect(string hash, [FromQuery] long expires, [FromQuery] string signature)
|
||||
{
|
||||
var result = await _cdnDownloadsService.GetDownloadAsync(hash, expires, signature).ConfigureAwait(false);
|
||||
|
||||
return result.Status switch
|
||||
{
|
||||
CDNDownloadsService.ResultStatus.Disabled => NotFound(),
|
||||
CDNDownloadsService.ResultStatus.Unauthorized => Unauthorized(),
|
||||
CDNDownloadsService.ResultStatus.NotFound => NotFound(),
|
||||
CDNDownloadsService.ResultStatus.Success => PhysicalFile(result.File!.FullName, "application/octet-stream"),
|
||||
_ => NotFound()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using LightlessSync.API.Routes;
|
||||
using LightlessSyncShared.Services;
|
||||
using LightlessSyncShared.Utils.Configuration;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace LightlessSyncStaticFilesServer.Services;
|
||||
|
||||
public class CDNDownloadUrlService
|
||||
{
|
||||
private readonly IConfigurationService<StaticFilesServerConfiguration> _staticConfig;
|
||||
private readonly IConfigurationService<LightlessConfigurationBase> _globalConfig;
|
||||
private readonly ILogger<CDNDownloadUrlService> _logger;
|
||||
|
||||
public CDNDownloadUrlService(IConfigurationService<StaticFilesServerConfiguration> staticConfig,
|
||||
IConfigurationService<LightlessConfigurationBase> globalConfig, ILogger<CDNDownloadUrlService> logger)
|
||||
{
|
||||
_staticConfig = staticConfig;
|
||||
_globalConfig = globalConfig;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool DirectDownloadsEnabled =>
|
||||
_staticConfig.GetValueOrDefault(nameof(StaticFilesServerConfiguration.EnableDirectDownloads), false);
|
||||
|
||||
public Uri? TryCreateDirectDownloadUri(Uri? baseUri, string hash)
|
||||
{
|
||||
if (!DirectDownloadsEnabled || baseUri == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!IsSupportedHash(hash))
|
||||
{
|
||||
_logger.LogDebug("Skipping direct download link generation for invalid hash {hash}", hash);
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalizedHash = hash.ToUpperInvariant();
|
||||
|
||||
var lifetimeSeconds = Math.Max(5,
|
||||
_staticConfig.GetValueOrDefault(nameof(StaticFilesServerConfiguration.DirectDownloadTokenLifetimeSeconds), 300));
|
||||
var expiresAt = DateTimeOffset.UtcNow.AddSeconds(lifetimeSeconds);
|
||||
var signature = CreateSignature(normalizedHash, expiresAt.ToUnixTimeSeconds());
|
||||
|
||||
var directPath = $"{LightlessFiles.ServerFiles}/{LightlessFiles.ServerFiles_DirectDownload}/{normalizedHash}";
|
||||
var builder = new UriBuilder(new Uri(baseUri, directPath));
|
||||
var query = new QueryBuilder
|
||||
{
|
||||
{ "expires", expiresAt.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture) },
|
||||
{ "signature", signature }
|
||||
};
|
||||
builder.Query = query.ToQueryString().Value!.TrimStart('?');
|
||||
return builder.Uri;
|
||||
}
|
||||
|
||||
public bool TryValidateSignature(string hash, long expiresUnixSeconds, string signature)
|
||||
{
|
||||
if (!DirectDownloadsEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(signature) || !IsSupportedHash(hash))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalizedHash = hash.ToUpperInvariant();
|
||||
|
||||
DateTimeOffset expiresAt;
|
||||
try
|
||||
{
|
||||
expiresAt = DateTimeOffset.FromUnixTimeSeconds(expiresUnixSeconds);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expiresAt < DateTimeOffset.UtcNow)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var expected = CreateSignature(normalizedHash, expiresAt.ToUnixTimeSeconds());
|
||||
return CryptographicOperations.FixedTimeEquals(
|
||||
Encoding.UTF8.GetBytes(expected),
|
||||
Encoding.UTF8.GetBytes(signature));
|
||||
}
|
||||
|
||||
private string CreateSignature(string hash, long expiresUnixSeconds)
|
||||
{
|
||||
var signingKey = _globalConfig.GetValue<string>(nameof(LightlessConfigurationBase.Jwt));
|
||||
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(signingKey));
|
||||
var payload = Encoding.UTF8.GetBytes($"{hash}:{expiresUnixSeconds}");
|
||||
return WebEncoders.Base64UrlEncode(hmac.ComputeHash(payload));
|
||||
}
|
||||
|
||||
private static bool IsSupportedHash(string hash)
|
||||
{
|
||||
return hash.Length == 40 && hash.All(char.IsAsciiLetterOrDigit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LightlessSyncStaticFilesServer.Services;
|
||||
|
||||
public class CDNDownloadsService
|
||||
{
|
||||
public enum ResultStatus
|
||||
{
|
||||
Disabled,
|
||||
Unauthorized,
|
||||
NotFound,
|
||||
Success
|
||||
}
|
||||
|
||||
public readonly record struct Result(ResultStatus Status, FileInfo? File);
|
||||
|
||||
private readonly CDNDownloadUrlService _cdnDownloadUrlService;
|
||||
private readonly CachedFileProvider _cachedFileProvider;
|
||||
|
||||
public CDNDownloadsService(CDNDownloadUrlService cdnDownloadUrlService, CachedFileProvider cachedFileProvider)
|
||||
{
|
||||
_cdnDownloadUrlService = cdnDownloadUrlService;
|
||||
_cachedFileProvider = cachedFileProvider;
|
||||
}
|
||||
|
||||
public bool DownloadsEnabled => _cdnDownloadUrlService.DirectDownloadsEnabled;
|
||||
|
||||
public async Task<Result> GetDownloadAsync(string hash, long expiresUnixSeconds, string signature)
|
||||
{
|
||||
if (!_cdnDownloadUrlService.DirectDownloadsEnabled)
|
||||
{
|
||||
return new Result(ResultStatus.Disabled, null);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(signature) || string.IsNullOrEmpty(hash))
|
||||
{
|
||||
return new Result(ResultStatus.Unauthorized, null);
|
||||
}
|
||||
|
||||
hash = hash.ToUpperInvariant();
|
||||
|
||||
if (!_cdnDownloadUrlService.TryValidateSignature(hash, expiresUnixSeconds, signature))
|
||||
{
|
||||
return new Result(ResultStatus.Unauthorized, null);
|
||||
}
|
||||
|
||||
var fileInfo = await _cachedFileProvider.DownloadAndGetLocalFileInfo(hash).ConfigureAwait(false);
|
||||
if (fileInfo == null)
|
||||
{
|
||||
return new Result(ResultStatus.NotFound, null);
|
||||
}
|
||||
|
||||
return new Result(ResultStatus.Success, fileInfo);
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,8 @@ public class Startup
|
||||
services.AddSingleton<RequestFileStreamResultFactory>();
|
||||
services.AddSingleton<ServerTokenGenerator>();
|
||||
services.AddSingleton<RequestQueueService>();
|
||||
services.AddSingleton<CDNDownloadUrlService>();
|
||||
services.AddSingleton<CDNDownloadsService>();
|
||||
services.AddHostedService(p => p.GetService<RequestQueueService>());
|
||||
services.AddHostedService(m => m.GetService<FileStatisticsService>());
|
||||
services.AddSingleton<IConfigurationService<LightlessConfigurationBase>, LightlessConfigurationServiceClient<LightlessConfigurationBase>>();
|
||||
@@ -204,7 +206,8 @@ public class Startup
|
||||
}
|
||||
else if (_isDistributionNode)
|
||||
{
|
||||
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(CacheController), typeof(RequestController), typeof(DistributionController), typeof(SpeedTestController)));
|
||||
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(CacheController), typeof(RequestController),
|
||||
typeof(DistributionController), typeof(ShardServerFilesController), typeof(SpeedTestController)));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
"UnusedFileRetentionPeriodInDays": 7,
|
||||
"CacheDirectory": "G:\\ServerTest",
|
||||
"ServiceAddress": "http://localhost:5002",
|
||||
"RemoteCacheSourceUri": ""
|
||||
"RemoteCacheSourceUri": "",
|
||||
"EnableDirectDownloads": true,
|
||||
"DirectDownloadTokenLifetimeSeconds": 300
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user