Compare commits
67 Commits
1.12.0-ser
...
sql-thing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7f7381dec | ||
| 1ce7a718bb | |||
|
|
46bb7a4a98 | ||
|
|
8752ce0e62 | ||
|
|
db0115316d | ||
|
|
00d4632510 | ||
|
|
e61e0db36b | ||
|
|
8d82365d0e | ||
|
|
b142329d09 | ||
|
|
8a329ccbaa | ||
|
|
23ee3f98b0 | ||
|
|
f8e711f3c0 | ||
|
|
73e7bb67bb | ||
|
|
70500b21e6 | ||
|
|
698a9eddf7 | ||
| 9cab73e8c8 | |||
| 5240beddf4 | |||
| cb4998e960 | |||
|
|
884ad25c33 | ||
|
|
3926f3be89 | ||
|
|
d28198a9c8 | ||
|
|
7cc6918b12 | ||
|
|
dba7536a7f | ||
|
|
f35c0c4c2a | ||
|
|
ad00f7b078 | ||
|
|
c30190704f | ||
|
|
bab81aaf51 | ||
|
|
4fdc2a5c29 | ||
|
|
bbcf98576e | ||
|
|
1ac92f6da2 | ||
|
|
e7e4a4527a | ||
| 583f1a8957 | |||
|
|
2ebdd6e0c7 | ||
|
|
2407259769 | ||
| 03af0b853c | |||
|
|
53f663fcbf | ||
|
|
47a94cb79f | ||
| f933b40368 | |||
|
|
b670cb69dd | ||
|
|
50f3b0d644 | ||
|
|
3a6203844e | ||
|
|
80086f6817 | ||
| 7e565ff85e | |||
|
|
49177e639e | ||
|
|
b36b1fb8f9 | ||
|
|
79483205f1 | ||
|
|
707c565ea9 | ||
|
|
6beda853f7 | ||
|
|
23dc6d7ef4 | ||
|
|
f686f7a6da | ||
|
|
280cc2ebbb | ||
|
|
7909850ad5 | ||
|
|
f60994fa58 | ||
|
|
96f230cd21 | ||
| 0fe1a43fb2 | |||
|
|
43b9c6f90e | ||
|
|
59f3739b9c | ||
| aadfaca629 | |||
| 729d781fa3 | |||
|
|
be95f24dcd | ||
|
|
a1f9526c23 | ||
|
|
0450255d6d | ||
|
|
b6907a2704 | ||
|
|
479b80a5a0 | ||
| d4d6e21381 | |||
|
|
3d9fc4fba0 | ||
| 58f5f3ad85 |
1
.gitmodules
vendored
1
.gitmodules
vendored
@@ -1,3 +1,4 @@
|
|||||||
[submodule "LightlessAPI"]
|
[submodule "LightlessAPI"]
|
||||||
path = LightlessAPI
|
path = LightlessAPI
|
||||||
url = https://git.lightless-sync.org/Lightless-Sync/LightlessAPI
|
url = https://git.lightless-sync.org/Lightless-Sync/LightlessAPI
|
||||||
|
branch = main
|
||||||
Submodule LightlessAPI updated: 167508d27b...bb92cd477d
@@ -100,14 +100,15 @@ public abstract class AuthControllerBase : Controller
|
|||||||
|
|
||||||
protected async Task<IActionResult> CreateJwtFromId(string uid, string charaIdent, string alias)
|
protected async Task<IActionResult> CreateJwtFromId(string uid, string charaIdent, string alias)
|
||||||
{
|
{
|
||||||
var token = CreateJwt(new List<Claim>()
|
var token = CreateJwt(
|
||||||
{
|
[
|
||||||
new Claim(LightlessClaimTypes.Uid, uid),
|
new Claim(LightlessClaimTypes.Uid, uid),
|
||||||
new Claim(LightlessClaimTypes.CharaIdent, charaIdent),
|
new Claim(LightlessClaimTypes.CharaIdent, charaIdent),
|
||||||
new Claim(LightlessClaimTypes.Alias, alias),
|
new Claim(LightlessClaimTypes.Alias, alias),
|
||||||
new Claim(LightlessClaimTypes.Expires, DateTime.UtcNow.AddHours(6).Ticks.ToString(CultureInfo.InvariantCulture)),
|
new Claim(LightlessClaimTypes.Expires, DateTime.UtcNow.AddHours(6).Ticks.ToString(CultureInfo.InvariantCulture)),
|
||||||
new Claim(LightlessClaimTypes.Continent, await _geoIPProvider.GetCountryFromIP(HttpAccessor))
|
new Claim(LightlessClaimTypes.Continent, await _geoIPProvider.GetContinentFromIP(HttpAccessor)),
|
||||||
});
|
new Claim(LightlessClaimTypes.Country, await _geoIPProvider.GetCountryFromIP(HttpAccessor)),
|
||||||
|
]);
|
||||||
|
|
||||||
return Content(token.RawData);
|
return Content(token.RawData);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using LightlessSync.API.Routes;
|
using LightlessSync.API.Routes;
|
||||||
using LightlessSyncAuthService.Services;
|
using LightlessSyncAuthService.Services;
|
||||||
|
using LightlessSyncAuthService.Utils;
|
||||||
using LightlessSyncShared;
|
using LightlessSyncShared;
|
||||||
using LightlessSyncShared.Data;
|
using LightlessSyncShared.Data;
|
||||||
using LightlessSyncShared.Services;
|
using LightlessSyncShared.Services;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using LightlessSync.API.Routes;
|
using LightlessSync.API.Routes;
|
||||||
using LightlessSyncAuthService.Services;
|
using LightlessSyncAuthService.Services;
|
||||||
|
using LightlessSyncAuthService.Utils;
|
||||||
using LightlessSyncShared;
|
using LightlessSyncShared;
|
||||||
using LightlessSyncShared.Data;
|
using LightlessSyncShared.Data;
|
||||||
using LightlessSyncShared.Services;
|
using LightlessSyncShared.Services;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
using LightlessSyncShared;
|
using LightlessSyncAuthService.Utils;
|
||||||
using LightlessSyncShared.Services;
|
using LightlessSyncShared.Services;
|
||||||
using LightlessSyncShared.Utils.Configuration;
|
using LightlessSyncShared.Utils.Configuration;
|
||||||
using MaxMind.GeoIP2;
|
using MaxMind.GeoIP2;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
namespace LightlessSyncAuthService.Services;
|
namespace LightlessSyncAuthService.Services;
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ public class GeoIPService : IHostedService
|
|||||||
_lightlessConfiguration = lightlessConfiguration;
|
_lightlessConfiguration = lightlessConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GetCountryFromIP(IHttpContextAccessor httpContextAccessor)
|
public async Task<string> GetContinentFromIP(IHttpContextAccessor httpContextAccessor)
|
||||||
{
|
{
|
||||||
if (!_useGeoIP)
|
if (!_useGeoIP)
|
||||||
{
|
{
|
||||||
@@ -32,7 +33,9 @@ public class GeoIPService : IHostedService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var ip = httpContextAccessor.GetIpAddress();
|
var ip = httpContextAccessor.GetClientIpAddress();
|
||||||
|
if (ip is null || IPAddress.IsLoopback(ip))
|
||||||
|
return "*";
|
||||||
|
|
||||||
using CancellationTokenSource waitCts = new();
|
using CancellationTokenSource waitCts = new();
|
||||||
waitCts.CancelAfter(TimeSpan.FromSeconds(5));
|
waitCts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||||
@@ -41,6 +44,7 @@ public class GeoIPService : IHostedService
|
|||||||
if (_dbReader!.TryCity(ip, out var response))
|
if (_dbReader!.TryCity(ip, out var response))
|
||||||
{
|
{
|
||||||
string? continent = response?.Continent.Code;
|
string? continent = response?.Continent.Code;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(continent) &&
|
if (!string.IsNullOrEmpty(continent) &&
|
||||||
string.Equals(continent, "NA", StringComparison.Ordinal)
|
string.Equals(continent, "NA", StringComparison.Ordinal)
|
||||||
&& response?.Location.Longitude != null)
|
&& response?.Location.Longitude != null)
|
||||||
@@ -140,4 +144,34 @@ public class GeoIPService : IHostedService
|
|||||||
_dbReader?.Dispose();
|
_dbReader?.Dispose();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal async Task<string> GetCountryFromIP(IHttpContextAccessor httpContextAccessor)
|
||||||
|
{
|
||||||
|
if (!_useGeoIP)
|
||||||
|
return "*";
|
||||||
|
|
||||||
|
var ip = httpContextAccessor.GetClientIpAddress();
|
||||||
|
if (ip is null || IPAddress.IsLoopback(ip))
|
||||||
|
return "*";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using CancellationTokenSource waitCts = new(TimeSpan.FromSeconds(5));
|
||||||
|
while (_processingReload)
|
||||||
|
await Task.Delay(100, waitCts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (_dbReader!.TryCity(ip, out var response))
|
||||||
|
{
|
||||||
|
var country = response?.Country?.IsoCode;
|
||||||
|
return country ?? "*";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "*";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "GeoIP lookup failed for {Ip}", ip);
|
||||||
|
return "*";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace LightlessSyncAuthService.Utils
|
||||||
|
{
|
||||||
|
public static class HttpContextAccessorExtensions
|
||||||
|
{
|
||||||
|
public static IPAddress? GetClientIpAddress(this IHttpContextAccessor accessor)
|
||||||
|
{
|
||||||
|
var context = accessor.HttpContext;
|
||||||
|
if (context == null) return null;
|
||||||
|
|
||||||
|
string[] headerKeys = { "CF-Connecting-IP", "X-Forwarded-For", "X-Real-IP" };
|
||||||
|
foreach (var key in headerKeys)
|
||||||
|
{
|
||||||
|
if (context.Request.Headers.TryGetValue(key, out var values))
|
||||||
|
{
|
||||||
|
var ipCandidate = values.FirstOrDefault()?.Split(',').FirstOrDefault()?.Trim();
|
||||||
|
if (IPAddress.TryParse(ipCandidate, out var parsed))
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.Connection?.RemoteIpAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,6 +59,14 @@ public class BroadcastConfiguration : IBroadcastConfiguration
|
|||||||
return string.Concat(RedisKeyPrefix, hashedCid);
|
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()
|
public string BuildPairRequestNotification()
|
||||||
{
|
{
|
||||||
var template = Options.PairRequestNotificationTemplate;
|
var template = Options.PairRequestNotificationTemplate;
|
||||||
@@ -69,4 +77,7 @@ public class BroadcastConfiguration : IBroadcastConfiguration
|
|||||||
|
|
||||||
return template;
|
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 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.";
|
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; }
|
bool EnableSyncshellBroadcastPayloads { get; }
|
||||||
|
|
||||||
string BuildRedisKey(string hashedCid);
|
string BuildRedisKey(string hashedCid);
|
||||||
|
string BuildUserOwnershipKey(string userUid);
|
||||||
string BuildPairRequestNotification();
|
string BuildPairRequestNotification();
|
||||||
|
|
||||||
|
int PairRequestRateLimit { get; }
|
||||||
|
int PairRequestRateWindow { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
using LightlessSyncShared.Models;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using LightlessSyncServer.Utils;
|
|
||||||
using LightlessSyncShared.Utils;
|
|
||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Dto.Group;
|
using LightlessSync.API.Dto.Group;
|
||||||
|
using LightlessSyncServer.Models;
|
||||||
|
using LightlessSyncServer.Utils;
|
||||||
using LightlessSyncShared.Metrics;
|
using LightlessSyncShared.Metrics;
|
||||||
|
using LightlessSyncShared.Models;
|
||||||
|
using LightlessSyncShared.Utils;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using System.Threading;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace LightlessSyncServer.Hubs;
|
namespace LightlessSyncServer.Hubs;
|
||||||
|
|
||||||
@@ -18,6 +20,8 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
public string Continent => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, LightlessClaimTypes.Continent, StringComparison.Ordinal))?.Value ?? "UNK";
|
public string Continent => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, LightlessClaimTypes.Continent, StringComparison.Ordinal))?.Value ?? "UNK";
|
||||||
|
|
||||||
|
public string Country => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, LightlessClaimTypes.Country, StringComparison.Ordinal))?.Value ?? "UNK";
|
||||||
|
|
||||||
private async Task DeleteUser(User user)
|
private async Task DeleteUser(User user)
|
||||||
{
|
{
|
||||||
var ownPairData = await DbContext.ClientPairs.Where(u => u.User.UID == user.UID).ToListAsync().ConfigureAwait(false);
|
var ownPairData = await DbContext.ClientPairs.Where(u => u.User.UID == user.UID).ToListAsync().ConfigureAwait(false);
|
||||||
@@ -95,13 +99,18 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
private async Task RemoveUserFromRedis()
|
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);
|
await _redis.RemoveAsync("UID:" + UserUID, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<User?> EnsureUserHasVanity(string uid, CancellationToken cancellationToken = default)
|
private async Task<User?> EnsureUserHasVanity(string uid, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
cancellationToken = cancellationToken == default && _contextAccessor.HttpContext != null
|
cancellationToken = cancellationToken == default && _contextAccessor.HttpContext != null
|
||||||
? _contextAccessor.HttpContext.RequestAborted
|
? RequestAbortedToken
|
||||||
: cancellationToken;
|
: cancellationToken;
|
||||||
|
|
||||||
var user = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid, cancellationToken).ConfigureAwait(false);
|
var user = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -120,6 +129,47 @@ public partial class LightlessHub
|
|||||||
return user;
|
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)
|
private async Task SendGroupDeletedToAll(List<GroupPair> groupUsers)
|
||||||
{
|
{
|
||||||
foreach (var pair in groupUsers)
|
foreach (var pair in groupUsers)
|
||||||
@@ -161,7 +211,7 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
if (isOwnerResult.ReferredGroup == null) return (false, null);
|
if (isOwnerResult.ReferredGroup == null) return (false, null);
|
||||||
|
|
||||||
var groupPairSelf = await DbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == UserUID).ConfigureAwait(false);
|
var groupPairSelf = await DbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid || g.Group.Alias == gid && g.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||||
if (groupPairSelf == null || !groupPairSelf.IsModerator) return (false, null);
|
if (groupPairSelf == null || !groupPairSelf.IsModerator) return (false, null);
|
||||||
|
|
||||||
return (true, isOwnerResult.ReferredGroup);
|
return (true, isOwnerResult.ReferredGroup);
|
||||||
@@ -169,7 +219,7 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
private async Task<(bool isValid, Group ReferredGroup)> TryValidateOwner(string gid)
|
private async Task<(bool isValid, Group ReferredGroup)> TryValidateOwner(string gid)
|
||||||
{
|
{
|
||||||
var group = await DbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
|
var group = await DbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid || g.Alias == gid).ConfigureAwait(false);
|
||||||
if (group == null) return (false, null);
|
if (group == null) return (false, null);
|
||||||
|
|
||||||
return (string.Equals(group.OwnerUID, UserUID, StringComparison.Ordinal), group);
|
return (string.Equals(group.OwnerUID, UserUID, StringComparison.Ordinal), group);
|
||||||
@@ -188,7 +238,13 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
private async Task UpdateUserOnRedis()
|
private async Task UpdateUserOnRedis()
|
||||||
{
|
{
|
||||||
await _redis.AddAsync("UID:" + UserUID, UserCharaIdent, TimeSpan.FromSeconds(60), StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false);
|
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)
|
private async Task UserGroupLeave(GroupPair groupUserPair, string userIdent, Dictionary<string, UserInfo> allUserPairs, string? uid = null)
|
||||||
|
|||||||
@@ -3,12 +3,18 @@ using LightlessSync.API.Data.Enum;
|
|||||||
using LightlessSync.API.Data.Extensions;
|
using LightlessSync.API.Data.Extensions;
|
||||||
using LightlessSync.API.Dto.Group;
|
using LightlessSync.API.Dto.Group;
|
||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
|
using LightlessSyncServer.Models;
|
||||||
|
using LightlessSyncServer.Services;
|
||||||
using LightlessSyncServer.Utils;
|
using LightlessSyncServer.Utils;
|
||||||
using LightlessSyncShared.Models;
|
using LightlessSyncShared.Models;
|
||||||
using LightlessSyncShared.Utils;
|
using LightlessSyncShared.Utils;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using System.Reflection;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace LightlessSyncServer.Hubs;
|
namespace LightlessSyncServer.Hubs;
|
||||||
@@ -59,7 +65,7 @@ public partial class LightlessHub
|
|||||||
group.PreferDisableAnimations = dto.Permissions.HasFlag(GroupPermissions.PreferDisableAnimations);
|
group.PreferDisableAnimations = dto.Permissions.HasFlag(GroupPermissions.PreferDisableAnimations);
|
||||||
group.PreferDisableVFX = dto.Permissions.HasFlag(GroupPermissions.PreferDisableVFX);
|
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();
|
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);
|
await Clients.Users(groupPairs).Client_GroupChangePermissions(new GroupPermissionDto(dto.Group, dto.Permissions)).ConfigureAwait(false);
|
||||||
@@ -137,7 +143,7 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
var allUserPairs = await GetAllPairInfo(pair.GroupUserUID).ConfigureAwait(false);
|
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);
|
DbContext.CharaDataAllowances.RemoveRange(sharedData);
|
||||||
|
|
||||||
foreach (var groupUserPair in groupPairs.Where(p => !string.Equals(p.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal)))
|
foreach (var groupUserPair in groupPairs.Where(p => !string.Equals(p.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal)))
|
||||||
@@ -181,7 +187,7 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
var allUserPairs = await GetAllPairInfo(pair.GroupUserUID).ConfigureAwait(false);
|
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);
|
DbContext.CharaDataAllowances.RemoveRange(sharedData);
|
||||||
|
|
||||||
@@ -199,15 +205,15 @@ public partial class LightlessHub
|
|||||||
public async Task<GroupJoinDto> GroupCreate()
|
public async Task<GroupJoinDto> GroupCreate()
|
||||||
{
|
{
|
||||||
_logger.LogCallInfo();
|
_logger.LogCallInfo();
|
||||||
var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == 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: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false);
|
var existingJoinedGroups = await DbContext.GroupPairs.CountAsync(u => u.GroupUserUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
|
||||||
if (existingGroupsByUser >= _maxExistingGroupsByUser || existingJoinedGroups >= _maxJoinedGroupsByUser)
|
if (existingGroupsByUser >= _maxExistingGroupsByUser || existingJoinedGroups >= _maxJoinedGroupsByUser)
|
||||||
{
|
{
|
||||||
throw new System.Exception($"Max groups for user is {_maxExistingGroupsByUser}, max joined groups is {_maxJoinedGroupsByUser}.");
|
throw new System.Exception($"Max groups for user is {_maxExistingGroupsByUser}, max joined groups is {_maxJoinedGroupsByUser}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var gid = StringUtils.GenerateRandomString(12);
|
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);
|
gid = StringUtils.GenerateRandomString(12);
|
||||||
}
|
}
|
||||||
@@ -218,7 +224,7 @@ public partial class LightlessHub
|
|||||||
var hashedPw = StringUtils.Sha256String(passwd);
|
var hashedPw = StringUtils.Sha256String(passwd);
|
||||||
var currentTime = DateTime.UtcNow;
|
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()
|
Group newGroup = new()
|
||||||
{
|
{
|
||||||
@@ -250,12 +256,12 @@ public partial class LightlessHub
|
|||||||
DisableVFX = defaultPermissions.DisableGroupAnimations,
|
DisableVFX = defaultPermissions.DisableGroupAnimations,
|
||||||
};
|
};
|
||||||
|
|
||||||
await DbContext.Groups.AddAsync(newGroup, _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false);
|
await DbContext.Groups.AddAsync(newGroup, RequestAbortedToken).ConfigureAwait(false);
|
||||||
await DbContext.GroupPairs.AddAsync(initialPair, _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false);
|
await DbContext.GroupPairs.AddAsync(initialPair, RequestAbortedToken).ConfigureAwait(false);
|
||||||
await DbContext.GroupPairPreferredPermissions.AddAsync(initialPrefPermissions, _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false);
|
await DbContext.GroupPairPreferredPermissions.AddAsync(initialPrefPermissions, RequestAbortedToken).ConfigureAwait(false);
|
||||||
await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).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(),
|
await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(),
|
||||||
newGroup.ToEnum(), initialPrefPermissions.ToEnum(), initialPair.ToEnum(), new(StringComparer.Ordinal)))
|
newGroup.ToEnum(), initialPrefPermissions.ToEnum(), initialPair.ToEnum(), new(StringComparer.Ordinal)))
|
||||||
@@ -314,10 +320,10 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
|
_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.RemoveRange(groupPairs);
|
||||||
DbContext.Remove(group);
|
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);
|
await Clients.Users(groupPairs.Select(g => g.GroupUserUID)).Client_GroupDelete(new GroupDto(group.ToGroupData())).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -332,7 +338,7 @@ public partial class LightlessHub
|
|||||||
var (userHasRights, group) = await TryValidateGroupModeratorOrOwner(dto.GID).ConfigureAwait(false);
|
var (userHasRights, group) = await TryValidateGroupModeratorOrOwner(dto.GID).ConfigureAwait(false);
|
||||||
if (!userHasRights) return [];
|
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<BannedGroupUserDto> bannedGroupUsers = banEntries.Select(b =>
|
List<BannedGroupUserDto> bannedGroupUsers = banEntries.Select(b =>
|
||||||
new BannedGroupUserDto(group.ToGroupData(), b.BannedUser.ToUserData(), b.BannedReason, b.BannedOn,
|
new BannedGroupUserDto(group.ToGroupData(), b.BannedUser.ToUserData(), b.BannedReason, b.BannedOn,
|
||||||
@@ -350,14 +356,14 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
_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 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 hashedPw = StringUtils.Sha256String(dto.Password);
|
||||||
var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid, 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: _contextAccessor.HttpContext.RequestAborted).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: _contextAccessor.HttpContext.RequestAborted).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: _contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false);
|
var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (group == null
|
if (group == null
|
||||||
|| (!string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal) && oneTimeInvite == null)
|
|| (!string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal) && oneTimeInvite == null)
|
||||||
@@ -378,7 +384,7 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
_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 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).ConfigureAwait(false);
|
||||||
var isHashedPassword = dto.Password.Length == 64 && dto.Password.All(Uri.IsHexDigit);
|
var isHashedPassword = dto.Password.Length == 64 && dto.Password.All(Uri.IsHexDigit);
|
||||||
@@ -416,7 +422,7 @@ public partial class LightlessHub
|
|||||||
FromFinder = isHashedPassword
|
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)
|
if (preferredPermissions == null)
|
||||||
{
|
{
|
||||||
GroupPairPreferredPermission newPerms = new()
|
GroupPairPreferredPermission newPerms = new()
|
||||||
@@ -441,13 +447,13 @@ public partial class LightlessHub
|
|||||||
DbContext.Update(preferredPermissions);
|
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"));
|
_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(),
|
await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), group.Owner.ToUserData(),
|
||||||
group.ToEnum(), preferredPermissions.ToEnum(), newPair.ToEnum(),
|
group.ToEnum(), preferredPermissions.ToEnum(), newPair.ToEnum(),
|
||||||
groupInfos.ToDictionary(u => u.GroupUserUID, u => u.ToEnum(), StringComparer.Ordinal))).ConfigureAwait(false);
|
groupInfos.ToDictionary(u => u.GroupUserUID, u => u.ToEnum(), StringComparer.Ordinal))).ConfigureAwait(false);
|
||||||
@@ -575,7 +581,7 @@ public partial class LightlessHub
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false);
|
await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -693,7 +699,7 @@ public partial class LightlessHub
|
|||||||
.Client_GroupPairLeft(new GroupPairDto(dto.Group, pair.GroupUser.ToUserData())).ConfigureAwait(false);
|
.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();
|
return usersToPrune.Count();
|
||||||
}
|
}
|
||||||
@@ -717,15 +723,15 @@ public partial class LightlessHub
|
|||||||
var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == group.GID).AsNoTracking().ToList();
|
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);
|
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);
|
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);
|
var userIdent = await GetUserIdent(dto.User.UID).ConfigureAwait(false);
|
||||||
if (userIdent == null)
|
if (userIdent == null)
|
||||||
{
|
{
|
||||||
await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false);
|
await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -743,29 +749,41 @@ public partial class LightlessHub
|
|||||||
{
|
{
|
||||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||||
|
|
||||||
var cancellationToken = _contextAccessor.HttpContext.RequestAborted;
|
var cancellationToken = RequestAbortedToken;
|
||||||
|
|
||||||
var data = await DbContext.GroupProfiles
|
if (dto?.Group == null)
|
||||||
.FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
var profileDto = new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null);
|
|
||||||
|
|
||||||
if (data is not null)
|
|
||||||
{
|
{
|
||||||
profileDto = profileDto with
|
_logger.LogCallWarning(LightlessHubLogger.Args("GroupGetProfile: dto.Group is null"));
|
||||||
{
|
return new GroupProfileDto(Group: null, Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: false, IsDisabled: false);
|
||||||
Description = data.Description,
|
|
||||||
Tags = data.Tags,
|
|
||||||
PictureBase64 = data.Base64GroupProfileImage,
|
|
||||||
};
|
|
||||||
|
|
||||||
await Clients.User(UserUID)
|
|
||||||
.Client_GroupSendProfile(profileDto)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return profileDto;
|
var data = await DbContext.GroupProfiles
|
||||||
|
.Include(gp => gp.Group)
|
||||||
|
.FirstOrDefaultAsync(
|
||||||
|
g => g.Group.GID == dto.Group.GID || g.Group.Alias == dto.Group.AliasOrGID,
|
||||||
|
cancellationToken
|
||||||
|
)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
return new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: false, IsDisabled: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.ProfileDisabled)
|
||||||
|
{
|
||||||
|
return new GroupProfileDto(Group: dto.Group, Description: "This profile was permanently disabled", Tags: [], PictureBase64: null, BannerBase64: null, IsNsfw: false, IsDisabled: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return data.ToDTO();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args(ex, "GroupGetProfile: failed to map GroupProfileDto for {Group}", dto.Group.GID ?? dto.Group.AliasOrGID));
|
||||||
|
return new GroupProfileDto(dto.Group, Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: false, IsDisabled: false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "Identified")]
|
[Authorize(Policy = "Identified")]
|
||||||
@@ -773,38 +791,82 @@ public partial class LightlessHub
|
|||||||
{
|
{
|
||||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||||
|
|
||||||
|
var cancellationToken = RequestAbortedToken;
|
||||||
|
|
||||||
if (dto.Group == null) return;
|
if (dto.Group == null) return;
|
||||||
|
|
||||||
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||||
if (!hasRights) return;
|
if (!hasRights) return;
|
||||||
|
|
||||||
var groupProfileDb = await DbContext.GroupProfiles
|
var groupProfileDb = await DbContext.GroupProfiles
|
||||||
.FirstOrDefaultAsync(g => g.GroupGID == dto.Group.GID,
|
.FirstOrDefaultAsync(g => g.Group.GID == dto.Group.GID || g.Group.Alias == dto.Group.GID,
|
||||||
_contextAccessor.HttpContext.RequestAborted)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (groupProfileDb != null)
|
ImageCheckService.ImageLoadResult profileResult = null;
|
||||||
|
ImageCheckService.ImageLoadResult bannerResult = null;
|
||||||
|
|
||||||
|
//Avatar image validation
|
||||||
|
if (!string.IsNullOrEmpty(dto.PictureBase64))
|
||||||
{
|
{
|
||||||
groupProfileDb.Description = dto.Description;
|
profileResult = await ImageCheckService.ValidateImageAsync(dto.PictureBase64, banner: false, RequestAbortedToken).ConfigureAwait(false);
|
||||||
groupProfileDb.Tags = dto.Tags;
|
|
||||||
groupProfileDb.Base64GroupProfileImage = dto.PictureBase64;
|
if (!profileResult.Success)
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, profileResult.ErrorMessage).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Banner image validation
|
||||||
|
if (!string.IsNullOrEmpty(dto.BannerBase64))
|
||||||
|
{
|
||||||
|
bannerResult = await ImageCheckService.ValidateImageAsync(dto.BannerBase64, banner: true, RequestAbortedToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!bannerResult.Success)
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, bannerResult.ErrorMessage).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupProfileDb == null)
|
||||||
|
{
|
||||||
|
groupProfileDb = new GroupProfile
|
||||||
|
{
|
||||||
|
GroupGID = dto.Group.GID,
|
||||||
|
ProfileDisabled = false,
|
||||||
|
IsNSFW = dto.IsNsfw ?? false,
|
||||||
|
};
|
||||||
|
|
||||||
|
groupProfileDb.UpdateProfileFromDto(dto, profileResult.Base64Image, bannerResult.Base64Image);
|
||||||
|
await DbContext.GroupProfiles.AddAsync(groupProfileDb, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var groupProfile = new GroupProfile
|
if (groupProfileDb?.ProfileDisabled ?? false)
|
||||||
{
|
{
|
||||||
GroupGID = dto.Group.GID,
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false);
|
||||||
Description = dto.Description,
|
return;
|
||||||
Tags = dto.Tags,
|
}
|
||||||
Base64GroupProfileImage = dto.PictureBase64,
|
|
||||||
};
|
|
||||||
|
|
||||||
await DbContext.GroupProfiles.AddAsync(groupProfile,
|
groupProfileDb.UpdateProfileFromDto(dto, profileResult.Base64Image, bannerResult.Base64Image);
|
||||||
_contextAccessor.HttpContext.RequestAborted)
|
|
||||||
|
var userIds = await DbContext.GroupPairs
|
||||||
|
.Where(p => p.GroupGID == groupProfileDb.GroupGID)
|
||||||
|
.Select(p => p.GroupUserUID)
|
||||||
|
.ToListAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (userIds.Count > 0)
|
||||||
|
{
|
||||||
|
var profileDto = groupProfileDb.ToDTO();
|
||||||
|
await Clients.Users(userIds).Client_GroupSendProfile(profileDto)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false);
|
await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "Identified")]
|
[Authorize(Policy = "Identified")]
|
||||||
@@ -836,9 +898,9 @@ public partial class LightlessHub
|
|||||||
userPair.IsModerator = false;
|
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);
|
await Clients.Users(groupPairs).Client_GroupPairChangeUserInfo(new GroupPairUserInfoDto(dto.Group, dto.User, userPair.ToEnum())).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -847,7 +909,7 @@ public partial class LightlessHub
|
|||||||
{
|
{
|
||||||
_logger.LogCallInfo();
|
_logger.LogCallInfo();
|
||||||
|
|
||||||
var ct = _contextAccessor.HttpContext.RequestAborted;
|
var ct = RequestAbortedToken;
|
||||||
|
|
||||||
var result = await (
|
var result = await (
|
||||||
from gp in DbContext.GroupPairs
|
from gp in DbContext.GroupPairs
|
||||||
@@ -866,7 +928,7 @@ public partial class LightlessHub
|
|||||||
.ToList(),
|
.ToList(),
|
||||||
})
|
})
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToListAsync(ct)
|
.ToListAsync()
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
_logger.LogCallInfo(LightlessHubLogger.Args(result));
|
_logger.LogCallInfo(LightlessHubLogger.Args(result));
|
||||||
@@ -899,11 +961,11 @@ public partial class LightlessHub
|
|||||||
var (userHasRights, _) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
var (userHasRights, _) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||||
if (!userHasRights) return;
|
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;
|
if (banEntry == null) return;
|
||||||
|
|
||||||
DbContext.Remove(banEntry);
|
DbContext.Remove(banEntry);
|
||||||
await DbContext.SaveChangesAsync(_contextAccessor.HttpContext.RequestAborted).ConfigureAwait(false);
|
await DbContext.SaveChangesAsync(RequestAbortedToken).ConfigureAwait(false);
|
||||||
|
|
||||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
|
_logger.LogCallInfo(LightlessHubLogger.Args(dto, "Success"));
|
||||||
}
|
}
|
||||||
@@ -990,6 +1052,4 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.API.Data.Extensions;
|
using LightlessSync.API.Data.Extensions;
|
||||||
using LightlessSync.API.Dto.Group;
|
using LightlessSync.API.Dto.Group;
|
||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
|
using LightlessSyncServer.Models;
|
||||||
|
using LightlessSyncServer.Services;
|
||||||
using LightlessSyncServer.Utils;
|
using LightlessSyncServer.Utils;
|
||||||
using LightlessSyncServer.Configuration;
|
|
||||||
using LightlessSyncShared.Metrics;
|
using LightlessSyncShared.Metrics;
|
||||||
using LightlessSyncShared.Models;
|
using LightlessSyncShared.Models;
|
||||||
using LightlessSyncShared.Utils;
|
using LightlessSyncShared.Utils;
|
||||||
@@ -14,7 +15,6 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@@ -35,7 +35,7 @@ public partial class LightlessHub
|
|||||||
if (string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal) || string.IsNullOrWhiteSpace(dto.User.UID)) return;
|
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
|
// 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)
|
if (otherUser == null)
|
||||||
{
|
{
|
||||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"Cannot pair with {dto.User.UID}, UID does not exist").ConfigureAwait(false);
|
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 =
|
var existingEntry =
|
||||||
await DbContext.ClientPairs.AsNoTracking()
|
await DbContext.ClientPairs.AsNoTracking()
|
||||||
.FirstOrDefaultAsync(p =>
|
.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)
|
if (existingEntry != null)
|
||||||
{
|
{
|
||||||
@@ -76,7 +76,7 @@ public partial class LightlessHub
|
|||||||
var permissions = existingData?.OwnPermissions;
|
var permissions = existingData?.OwnPermissions;
|
||||||
if (permissions == null || !permissions.Sticky)
|
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()
|
permissions = new UserPermissionSet()
|
||||||
{
|
{
|
||||||
@@ -89,7 +89,7 @@ public partial class LightlessHub
|
|||||||
Sticky = true
|
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)
|
if (existingDbPerms == null)
|
||||||
{
|
{
|
||||||
await DbContext.Permissions.AddAsync(permissions).ConfigureAwait(false);
|
await DbContext.Permissions.AddAsync(permissions).ConfigureAwait(false);
|
||||||
@@ -143,17 +143,15 @@ public partial class LightlessHub
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "Identified")]
|
[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))
|
if (string.IsNullOrWhiteSpace(otherCid) || string.IsNullOrWhiteSpace(myCid))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
bool IsValidCid(string cid) => cid.Length == 64 && cid.All(Uri.IsHexDigit) && !cid.All(c => c == '0');
|
if (!IsValidHashedCid(myCid) || !IsValidHashedCid(otherCid))
|
||||||
|
|
||||||
if (!IsValidCid(myCid) || !IsValidCid(otherCid))
|
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(otherCid, myCid, StringComparison.Ordinal))
|
if (string.Equals(otherCid, myCid, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -181,14 +179,35 @@ public partial class LightlessHub
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var payload = JsonSerializer.Deserialize<PairingPayload>(json);
|
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));
|
_logger.LogCallWarning(LightlessHubLogger.Args("invalid payload", reverseKey));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sender = await _pairService.TryAddPairAsync(UserUID, payload.UID);
|
if (!IsValidHashedCid(payload.HashedCid) || !string.Equals(payload.HashedCid, otherCid, StringComparison.Ordinal))
|
||||||
var receiver = await _pairService.TryAddPairAsync(payload.UID, UserUID);
|
{
|
||||||
|
_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).ConfigureAwait(false);
|
||||||
|
var receiver = await _pairService.TryAddPairAsync(payload.UID, UserUID).ConfigureAwait(false);
|
||||||
|
|
||||||
var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
||||||
var otherUser = await DbContext.Users.SingleAsync(u => u.UID == payload.UID).ConfigureAwait(false);
|
var otherUser = await DbContext.Users.SingleAsync(u => u.UID == payload.UID).ConfigureAwait(false);
|
||||||
@@ -251,9 +270,30 @@ public partial class LightlessHub
|
|||||||
}
|
}
|
||||||
else
|
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
|
var payload = new PairingPayload
|
||||||
{
|
{
|
||||||
UID = UserUID,
|
UID = UserUID,
|
||||||
|
HashedCid = myCid,
|
||||||
Timestamp = DateTime.UtcNow
|
Timestamp = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -262,14 +302,15 @@ public partial class LightlessHub
|
|||||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, $"Pair request sent. Waiting for the other player to confirm.").ConfigureAwait(false);
|
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));
|
_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 targetHashedCid)
|
||||||
private async Task NotifyBroadcastOwnerOfPairRequest(string myHashedCid, string targetHashedCid)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(targetHashedCid) || string.IsNullOrWhiteSpace(myHashedCid))
|
var myHashedCid = UserCharaIdent;
|
||||||
|
|
||||||
|
if (!IsValidHashedCid(targetHashedCid) || !IsValidHashedCid(myHashedCid))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!_broadcastConfiguration.EnableBroadcasting || !_broadcastConfiguration.NotifyOwnerOnPairRequest)
|
if (!_broadcastConfiguration.EnableBroadcasting || !_broadcastConfiguration.NotifyOwnerOnPairRequest)
|
||||||
@@ -320,26 +361,12 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
await Clients.User(entry.OwnerUID).Client_ReceiveBroadcastPairRequest(dto).ConfigureAwait(false);
|
await Clients.User(entry.OwnerUID).Client_ReceiveBroadcastPairRequest(dto).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
private class PairingPayload
|
|
||||||
{
|
|
||||||
public string UID { get; set; } = string.Empty;
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BroadcastRedisEntry
|
|
||||||
{
|
|
||||||
public string HashedCID { get; set; } = string.Empty;
|
|
||||||
public string OwnerUID { get; set; } = string.Empty;
|
|
||||||
public string? GID { get; set; }
|
|
||||||
|
|
||||||
public bool OwnedBy(string userUid) => !string.IsNullOrEmpty(userUid) && string.Equals(OwnerUID, userUid, StringComparison.Ordinal);
|
|
||||||
|
|
||||||
public bool HasOwner() => !string.IsNullOrEmpty(OwnerUID);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Authorize(Policy = "Identified")]
|
[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)
|
if (enabled && !_broadcastConfiguration.EnableBroadcasting)
|
||||||
{
|
{
|
||||||
_logger.LogCallWarning(LightlessHubLogger.Args("broadcast disabled", UserUID, "CID", hashedCid));
|
_logger.LogCallWarning(LightlessHubLogger.Args("broadcast disabled", UserUID, "CID", hashedCid));
|
||||||
@@ -347,9 +374,9 @@ public partial class LightlessHub
|
|||||||
return;
|
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);
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Invalid CID format.").ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -361,11 +388,32 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
var db = _redis.Database;
|
var db = _redis.Database;
|
||||||
var broadcastKey = _broadcastConfiguration.BuildRedisKey(hashedCid);
|
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)
|
if (enabled)
|
||||||
{
|
{
|
||||||
string? gid = null;
|
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 (groupDto is not null)
|
||||||
{
|
{
|
||||||
if (!_broadcastConfiguration.EnableSyncshellBroadcastPayloads)
|
if (!_broadcastConfiguration.EnableSyncshellBroadcastPayloads)
|
||||||
@@ -400,7 +448,7 @@ public partial class LightlessHub
|
|||||||
if (existingEntry is not null && existingEntry.HasOwner() && !existingEntry.OwnedBy(UserUID))
|
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));
|
_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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,6 +462,7 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
var json = JsonSerializer.Serialize(entry);
|
var json = JsonSerializer.Serialize(entry);
|
||||||
await db.StringSetAsync(broadcastKey, json, _broadcastConfiguration.BroadcastEntryTtl).ConfigureAwait(false);
|
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));
|
_logger.LogCallInfo(LightlessHubLogger.Args("broadcast enabled", hashedCid, "GID", gid));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -436,22 +485,81 @@ public partial class LightlessHub
|
|||||||
if (entry is null || !string.Equals(entry.HashedCID, hashedCid, StringComparison.Ordinal))
|
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));
|
_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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.HasOwner() && !entry.OwnedBy(UserUID))
|
if (entry.HasOwner() && !entry.OwnedBy(UserUID))
|
||||||
{
|
{
|
||||||
_logger.LogCallWarning(LightlessHubLogger.Args("unauthorized attempt to remove broadcast", UserUID, "CID", hashedCid, "Owner", entry.OwnerUID));
|
_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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.KeyDeleteAsync(broadcastKey).ConfigureAwait(false);
|
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));
|
_logger.LogCallInfo(LightlessHubLogger.Args("broadcast disabled", hashedCid, "GID", entry.GID));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<(BroadcastRedisEntry? Entry, TimeSpan? Expiry)> TryGetBroadcastEntryAsync(string hashedCid)
|
||||||
|
{
|
||||||
|
var key = _broadcastConfiguration.BuildRedisKey(hashedCid);
|
||||||
|
RedisValueWithExpiry value;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
value = await _redis.Database.StringGetWithExpiryAsync(key).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("LightfinderProfileLookupFailed", "CID", hashedCid, "Error", ex));
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.Value.IsNullOrEmpty || value.Expiry is null || value.Expiry <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
return (null, value.Expiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
BroadcastRedisEntry? entry;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
entry = JsonSerializer.Deserialize<BroadcastRedisEntry>(value.Value!);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("LightfinderProfileDeserializeFailed", "CID", hashedCid, "Raw", value.Value.ToString(), "Error", ex));
|
||||||
|
return (null, value.Expiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry is null || !string.Equals(entry.HashedCID, hashedCid, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("LightfinderProfileEntryMismatch", "CID", hashedCid, "EntryCID", entry?.HashedCID ?? "null"));
|
||||||
|
return (null, value.Expiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (entry, value.Expiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasActiveBroadcast(BroadcastRedisEntry? entry, TimeSpan? expiry) =>
|
||||||
|
entry?.HasOwner() == true && expiry.HasValue && expiry.Value > TimeSpan.Zero;
|
||||||
|
|
||||||
|
private static bool IsActiveBroadcastForUser(BroadcastRedisEntry? entry, TimeSpan? expiry, string userUid) =>
|
||||||
|
HasActiveBroadcast(entry, expiry) && entry!.OwnedBy(userUid);
|
||||||
|
|
||||||
|
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")]
|
[Authorize(Policy = "Identified")]
|
||||||
public async Task<BroadcastStatusInfoDto?> IsUserBroadcasting(string hashedCid)
|
public async Task<BroadcastStatusInfoDto?> IsUserBroadcasting(string hashedCid)
|
||||||
@@ -459,18 +567,13 @@ public partial class LightlessHub
|
|||||||
if (!_broadcastConfiguration.EnableBroadcasting)
|
if (!_broadcastConfiguration.EnableBroadcasting)
|
||||||
return null;
|
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));
|
_logger.LogCallWarning(LightlessHubLogger.Args("invalid cid format", UserUID, "CID", hashedCid));
|
||||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Invalid CID format.").ConfigureAwait(false);
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Invalid CID format.").ConfigureAwait(false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hashedCid.All(c => c == '0'))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var db = _redis.Database;
|
var db = _redis.Database;
|
||||||
var key = _broadcastConfiguration.BuildRedisKey(hashedCid);
|
var key = _broadcastConfiguration.BuildRedisKey(hashedCid);
|
||||||
|
|
||||||
@@ -507,14 +610,16 @@ public partial class LightlessHub
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "Identified")]
|
[Authorize(Policy = "Identified")]
|
||||||
public async Task<TimeSpan?> GetBroadcastTtl(string hashedCid)
|
public async Task<TimeSpan?> GetBroadcastTtl()
|
||||||
{
|
{
|
||||||
if (!_broadcastConfiguration.EnableBroadcasting)
|
if (!_broadcastConfiguration.EnableBroadcasting)
|
||||||
return null;
|
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);
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Invalid CID format.").ConfigureAwait(false);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -587,9 +692,7 @@ public partial class LightlessHub
|
|||||||
var tasks = new Dictionary<string, Task<RedisValueWithExpiry>>(hashedCids.Count);
|
var tasks = new Dictionary<string, Task<RedisValueWithExpiry>>(hashedCids.Count);
|
||||||
foreach (var cid in hashedCids)
|
foreach (var cid in hashedCids)
|
||||||
{
|
{
|
||||||
bool validHash = !string.IsNullOrWhiteSpace(cid) && cid.Length == 64 && cid.All(Uri.IsHexDigit) && !cid.All(c => c == '0');
|
if (!IsValidHashedCid(cid))
|
||||||
|
|
||||||
if (!validHash)
|
|
||||||
{
|
{
|
||||||
tasks[cid] = Task.FromResult(new RedisValueWithExpiry(RedisValue.Null, null));
|
tasks[cid] = Task.FromResult(new RedisValueWithExpiry(RedisValue.Null, null));
|
||||||
continue;
|
continue;
|
||||||
@@ -657,7 +760,7 @@ public partial class LightlessHub
|
|||||||
{
|
{
|
||||||
_logger.LogCallInfo();
|
_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);
|
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)
|
foreach (var user in secondaryUsers)
|
||||||
{
|
{
|
||||||
@@ -707,16 +810,107 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
if (!allUserPairs.Contains(user.User.UID, StringComparer.Ordinal) && !string.Equals(user.User.UID, UserUID, StringComparison.Ordinal))
|
if (!allUserPairs.Contains(user.User.UID, StringComparer.Ordinal) && !string.Equals(user.User.UID, UserUID, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return new UserProfileDto(user.User, false, null, null, "Due to the pause status you cannot access this users profile.");
|
return new UserProfileDto(user.User, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: "Due to the pause status you cannot access this users profile.", Tags: []);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 == null) return new UserProfileDto(user.User, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: null, Tags: []);
|
||||||
|
|
||||||
if (data.FlaggedForReport) return new UserProfileDto(user.User, true, null, null, "This profile is flagged for report and pending evaluation");
|
if (data.FlaggedForReport) return new UserProfileDto(user.User, Disabled: true, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: "This profile is flagged for report and pending evaluation", Tags: []);
|
||||||
if (data.ProfileDisabled) return new UserProfileDto(user.User, true, null, null, "This profile was permanently disabled");
|
if (data.ProfileDisabled) return new UserProfileDto(user.User, Disabled: true, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: "This profile was permanently disabled", Tags: []);
|
||||||
|
|
||||||
return new UserProfileDto(user.User, false, data.IsNSFW, data.Base64ProfileImage, data.UserDescription);
|
return data.ToDTO();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(Policy = "Identified")]
|
||||||
|
public async Task<UserProfileDto?> UserGetLightfinderProfile(string hashedCid)
|
||||||
|
{
|
||||||
|
_logger.LogCallInfo(LightlessHubLogger.Args("LightfinderProfile", hashedCid));
|
||||||
|
|
||||||
|
if (!_broadcastConfiguration.EnableBroadcasting)
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Lightfinder is currently disabled.").ConfigureAwait(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsValidHashedCid(hashedCid))
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("LightfinderProfileInvalidCid", hashedCid));
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "Invalid Lightfinder target.").ConfigureAwait(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var viewerCid = UserCharaIdent;
|
||||||
|
if (!IsValidHashedCid(viewerCid))
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You must be using Lightfinder to open player profiles.").ConfigureAwait(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (viewerEntry, viewerExpiry) = await TryGetBroadcastEntryAsync(viewerCid).ConfigureAwait(false);
|
||||||
|
if (!IsActiveBroadcastForUser(viewerEntry, viewerExpiry, UserUID))
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "You must be using Lightfinder to open player profiles.").ConfigureAwait(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (targetEntry, targetExpiry) = await TryGetBroadcastEntryAsync(hashedCid).ConfigureAwait(false);
|
||||||
|
if (!HasActiveBroadcast(targetEntry, targetExpiry))
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "That player is not currently using Lightfinder.").ConfigureAwait(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(targetEntry!.OwnerUID))
|
||||||
|
{
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "That player is not currently using Lightfinder.").ConfigureAwait(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetUser = await DbContext.Users.AsNoTracking()
|
||||||
|
.SingleOrDefaultAsync(u => u.UID == targetEntry.OwnerUID, cancellationToken: RequestAbortedToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (targetUser == null)
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("LightfinderProfileMissingUser", hashedCid, "OwnerUID", targetEntry.OwnerUID));
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, "Unable to load the players profile at this time.").ConfigureAwait(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayAlias = string.IsNullOrWhiteSpace(targetUser.Alias)
|
||||||
|
? "LightfinderUser"
|
||||||
|
: targetUser.Alias;
|
||||||
|
|
||||||
|
var userData = new UserData(
|
||||||
|
UID: hashedCid,
|
||||||
|
Alias: displayAlias,
|
||||||
|
IsAdmin: false,
|
||||||
|
IsModerator: false,
|
||||||
|
HasVanity: false,
|
||||||
|
TextColorHex: targetUser.TextColorHex,
|
||||||
|
TextGlowColorHex: targetUser.TextGlowColorHex);
|
||||||
|
|
||||||
|
var profile = await DbContext.UserProfileData.AsNoTracking()
|
||||||
|
.SingleOrDefaultAsync(u => u.UserUID == targetEntry.OwnerUID, cancellationToken: RequestAbortedToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (profile == null)
|
||||||
|
{
|
||||||
|
return new UserProfileDto(userData, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: null, Tags: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile.FlaggedForReport)
|
||||||
|
{
|
||||||
|
return new UserProfileDto(userData, Disabled: true, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: "This profile is flagged for report and pending evaluation", Tags: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile.ProfileDisabled)
|
||||||
|
{
|
||||||
|
return new UserProfileDto(userData, Disabled: true, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: "This profile was permanently disabled", Tags: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return profile.ToDTO();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "Identified")]
|
[Authorize(Policy = "Identified")]
|
||||||
@@ -749,22 +943,36 @@ public partial class LightlessHub
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool hadInvalidData = false;
|
bool hadInvalidData = false;
|
||||||
List<string> invalidGamePaths = new();
|
List<string> invalidGamePaths = [];
|
||||||
List<string> invalidFileSwapPaths = new();
|
List<string> invalidFileSwapPaths = [];
|
||||||
|
|
||||||
|
var gamePathRegex = GamePathRegex();
|
||||||
|
var hashRegex = HashRegex();
|
||||||
|
|
||||||
foreach (var replacement in dto.CharaData.FileReplacements.SelectMany(p => p.Value))
|
foreach (var replacement in dto.CharaData.FileReplacements.SelectMany(p => p.Value))
|
||||||
{
|
{
|
||||||
var invalidPaths = replacement.GamePaths.Where(p => !GamePathRegex().IsMatch(p)).ToList();
|
var validGamePaths = replacement.GamePaths
|
||||||
invalidPaths.AddRange(replacement.GamePaths.Where(p => !AllowedExtensionsForGamePaths.Any(e => p.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
|
.Where(p => gamePathRegex.IsMatch(p) &&
|
||||||
replacement.GamePaths = replacement.GamePaths.Where(p => !invalidPaths.Contains(p, StringComparer.OrdinalIgnoreCase)).ToArray();
|
AllowedExtensionsForGamePaths.Any(e => p.EndsWith(e, StringComparison.OrdinalIgnoreCase)))
|
||||||
bool validGamePaths = replacement.GamePaths.Any();
|
.ToArray();
|
||||||
bool validHash = string.IsNullOrEmpty(replacement.Hash) || HashRegex().IsMatch(replacement.Hash);
|
|
||||||
bool validFileSwapPath = string.IsNullOrEmpty(replacement.FileSwapPath) || GamePathRegex().IsMatch(replacement.FileSwapPath);
|
var invalidPaths = replacement.GamePaths.Except(validGamePaths, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||||
if (!validGamePaths || !validHash || !validFileSwapPath)
|
|
||||||
|
replacement.GamePaths = validGamePaths;
|
||||||
|
|
||||||
|
bool validHash = string.IsNullOrEmpty(replacement.Hash) || hashRegex.IsMatch(replacement.Hash);
|
||||||
|
bool validFileSwapPath = string.IsNullOrEmpty(replacement.FileSwapPath) || gamePathRegex.IsMatch(replacement.FileSwapPath);
|
||||||
|
bool validGamePathsFlag = validGamePaths.Length
|
||||||
|
!= 0;
|
||||||
|
|
||||||
|
if (!validGamePathsFlag || !validHash || !validFileSwapPath)
|
||||||
{
|
{
|
||||||
_logger.LogCallWarning(LightlessHubLogger.Args("Invalid Data", "GamePaths", validGamePaths, string.Join(",", invalidPaths), "Hash", validHash, replacement.Hash, "FileSwap", validFileSwapPath, replacement.FileSwapPath));
|
_logger.LogCallWarning(LightlessHubLogger.Args("Invalid Data", "GamePaths", validGamePathsFlag, string.Join(',', invalidPaths), "Hash", validHash, replacement.Hash, "FileSwap", validFileSwapPath, replacement.FileSwapPath));
|
||||||
|
|
||||||
hadInvalidData = true;
|
hadInvalidData = true;
|
||||||
|
|
||||||
if (!validFileSwapPath) invalidFileSwapPaths.Add(replacement.FileSwapPath);
|
if (!validFileSwapPath) invalidFileSwapPaths.Add(replacement.FileSwapPath);
|
||||||
if (!validGamePaths) invalidGamePaths.AddRange(replacement.GamePaths);
|
if (!validGamePathsFlag) invalidGamePaths.AddRange(invalidPaths);
|
||||||
if (!validHash) invalidFileSwapPaths.Add(replacement.Hash);
|
if (!validHash) invalidFileSwapPaths.Add(replacement.Hash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -867,7 +1075,7 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
// check if client pair even exists
|
// check if client pair even exists
|
||||||
ClientPair callerPair =
|
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;
|
if (callerPair == null) return;
|
||||||
|
|
||||||
var pairData = await GetPairInfo(UserUID, dto.User.UID).ConfigureAwait(false);
|
var pairData = await GetPairInfo(UserUID, dto.User.UID).ConfigureAwait(false);
|
||||||
@@ -916,76 +1124,72 @@ public partial class LightlessHub
|
|||||||
{
|
{
|
||||||
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
_logger.LogCallInfo(LightlessHubLogger.Args(dto));
|
||||||
|
|
||||||
|
var cancellationToken = RequestAbortedToken;
|
||||||
|
|
||||||
if (!string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) throw new HubException("Cannot modify profile data for anyone but yourself");
|
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)
|
ImageCheckService.ImageLoadResult profileResult = new();
|
||||||
{
|
ImageCheckService.ImageLoadResult bannerResult = new();
|
||||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile is currently flagged for report and cannot be edited").ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingData?.ProfileDisabled ?? false)
|
|
||||||
{
|
|
||||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
//Avatar image validation
|
||||||
if (!string.IsNullOrEmpty(dto.ProfilePictureBase64))
|
if (!string.IsNullOrEmpty(dto.ProfilePictureBase64))
|
||||||
{
|
{
|
||||||
byte[] imageData = Convert.FromBase64String(dto.ProfilePictureBase64);
|
profileResult = await ImageCheckService.ValidateImageAsync(dto.ProfilePictureBase64, banner: false, RequestAbortedToken).ConfigureAwait(false);
|
||||||
using MemoryStream ms = new(imageData);
|
|
||||||
var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false);
|
if (!profileResult.Success)
|
||||||
if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is not in PNG format").ConfigureAwait(false);
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, profileResult.ErrorMessage).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
using var image = Image.Load<Rgba32>(imageData);
|
}
|
||||||
|
|
||||||
if (image.Width > 256 || image.Height > 256 || (imageData.Length > 250 * 1024))
|
//Banner image validation
|
||||||
|
if (!string.IsNullOrEmpty(dto.BannerPictureBase64))
|
||||||
|
{
|
||||||
|
bannerResult = await ImageCheckService.ValidateImageAsync(dto.BannerPictureBase64, banner: true, RequestAbortedToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!bannerResult.Success)
|
||||||
{
|
{
|
||||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is larger than 256x256 or more than 250KiB.").ConfigureAwait(false);
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, bannerResult.ErrorMessage).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingData != null)
|
if (existingData != null)
|
||||||
{
|
{
|
||||||
if (string.Equals("", dto.ProfilePictureBase64, StringComparison.OrdinalIgnoreCase))
|
if (existingData.FlaggedForReport)
|
||||||
{
|
{
|
||||||
existingData.Base64ProfileImage = null;
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile is currently flagged for report and cannot be edited").ConfigureAwait(false);
|
||||||
}
|
return;
|
||||||
else if (dto.ProfilePictureBase64 != null)
|
|
||||||
{
|
|
||||||
existingData.Base64ProfileImage = dto.ProfilePictureBase64;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.IsNSFW != null)
|
if (existingData.ProfileDisabled)
|
||||||
{
|
{
|
||||||
existingData.IsNSFW = dto.IsNSFW.Value;
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.Description != null)
|
existingData.UpdateProfileFromDto(dto, profileResult.Base64Image, bannerResult.Base64Image);
|
||||||
{
|
|
||||||
existingData.UserDescription = dto.Description;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UserProfileData userProfileData = new()
|
UserProfileData newUserProfileData = new()
|
||||||
{
|
{
|
||||||
UserUID = dto.User.UID,
|
UserUID = dto.User.UID,
|
||||||
Base64ProfileImage = dto.ProfilePictureBase64 ?? null,
|
Base64ProfileImage = dto.ProfilePictureBase64 ?? null,
|
||||||
UserDescription = dto.Description ?? null,
|
UserDescription = dto.Description ?? null,
|
||||||
IsNSFW = dto.IsNSFW ?? false
|
IsNSFW = dto.IsNSFW ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
await DbContext.UserProfileData.AddAsync(userProfileData).ConfigureAwait(false);
|
existingData.UpdateProfileFromDto(dto, profileResult.Base64Image, bannerResult.Base64Image);
|
||||||
|
|
||||||
|
await DbContext.UserProfileData.AddAsync(newUserProfileData, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
|
||||||
|
await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||||
var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false);
|
var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ public partial class LightlessHub : Hub<ILightlessHub>, ILightlessHub
|
|||||||
private readonly int _maxCharaDataByUser;
|
private readonly int _maxCharaDataByUser;
|
||||||
private readonly int _maxCharaDataByUserVanity;
|
private readonly int _maxCharaDataByUserVanity;
|
||||||
|
|
||||||
|
private CancellationToken RequestAbortedToken => _contextAccessor.HttpContext?.RequestAborted ?? Context?.ConnectionAborted ?? CancellationToken.None;
|
||||||
|
|
||||||
public LightlessHub(LightlessMetrics lightlessMetrics,
|
public LightlessHub(LightlessMetrics lightlessMetrics,
|
||||||
IDbContextFactory<LightlessDbContext> lightlessDbContextFactory, ILogger<LightlessHub> logger, SystemInfoService systemInfoService,
|
IDbContextFactory<LightlessDbContext> lightlessDbContextFactory, ILogger<LightlessHub> logger, SystemInfoService systemInfoService,
|
||||||
IConfigurationService<ServerConfiguration> configuration, IHttpContextAccessor contextAccessor,
|
IConfigurationService<ServerConfiguration> configuration, IHttpContextAccessor contextAccessor,
|
||||||
@@ -158,8 +160,13 @@ public partial class LightlessHub : Hub<ILightlessHub>, ILightlessHub
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_lightlessMetrics.IncGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent);
|
var ResultLabels = new List<string>
|
||||||
|
{
|
||||||
|
Continent,
|
||||||
|
Country,
|
||||||
|
};
|
||||||
|
|
||||||
|
_lightlessMetrics.IncGaugeWithLabels(MetricsAPI.GaugeConnections, labels: [.. ResultLabels]);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogCallInfo(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, UserCharaIdent));
|
_logger.LogCallInfo(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, UserCharaIdent));
|
||||||
@@ -182,7 +189,12 @@ public partial class LightlessHub : Hub<ILightlessHub>, ILightlessHub
|
|||||||
if (_userConnections.TryGetValue(UserUID, out var connectionId)
|
if (_userConnections.TryGetValue(UserUID, out var connectionId)
|
||||||
&& string.Equals(connectionId, Context.ConnectionId, StringComparison.Ordinal))
|
&& string.Equals(connectionId, Context.ConnectionId, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
_lightlessMetrics.DecGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent);
|
var ResultLabels = new List<string>
|
||||||
|
{
|
||||||
|
Continent,
|
||||||
|
Country,
|
||||||
|
};
|
||||||
|
_lightlessMetrics.DecGaugeWithLabels(MetricsAPI.GaugeConnections, labels: [.. ResultLabels]);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -194,6 +206,8 @@ public partial class LightlessHub : Hub<ILightlessHub>, ILightlessHub
|
|||||||
if (exception != null)
|
if (exception != null)
|
||||||
_logger.LogCallWarning(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, exception.Message, exception.StackTrace));
|
_logger.LogCallWarning(LightlessHubLogger.Args(_contextAccessor.GetIpAddress(), Context.ConnectionId, exception.Message, exception.StackTrace));
|
||||||
|
|
||||||
|
await ClearOwnedBroadcastLock().ConfigureAwait(false);
|
||||||
|
|
||||||
await RemoveUserFromRedis().ConfigureAwait(false);
|
await RemoveUserFromRedis().ConfigureAwait(false);
|
||||||
|
|
||||||
_lightlessCensus.ClearStatistics(UserUID);
|
_lightlessCensus.ClearStatistics(UserUID);
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace LightlessSyncServer.Models;
|
||||||
|
|
||||||
|
public class BroadcastRedisEntry()
|
||||||
|
{
|
||||||
|
public string? GID { get; set; }
|
||||||
|
public string HashedCID { get; set; } = string.Empty;
|
||||||
|
public string OwnerUID { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool OwnedBy(string userUid) => !string.IsNullOrEmpty(userUid) && string.Equals(OwnerUID, userUid, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
public bool HasOwner() => !string.IsNullOrEmpty(OwnerUID);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace LightlessSyncServer.Models;
|
||||||
|
|
||||||
|
public class PairingPayload
|
||||||
|
{
|
||||||
|
public string UID { get; set; } = string.Empty;
|
||||||
|
public string HashedCid { get; set; } = string.Empty;
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
}
|
||||||
@@ -41,7 +41,6 @@ public class Program
|
|||||||
metrics.SetGaugeTo(MetricsAPI.GaugeUsersRegistered, context.Users.AsNoTracking().Count());
|
metrics.SetGaugeTo(MetricsAPI.GaugeUsersRegistered, context.Users.AsNoTracking().Count());
|
||||||
metrics.SetGaugeTo(MetricsAPI.GaugePairs, context.ClientPairs.AsNoTracking().Count());
|
metrics.SetGaugeTo(MetricsAPI.GaugePairs, context.ClientPairs.AsNoTracking().Count());
|
||||||
metrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, context.Permissions.AsNoTracking().Where(p=>p.IsPaused).Count());
|
metrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, context.Permissions.AsNoTracking().Where(p=>p.IsPaused).Count());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.Length == 0 || !string.Equals(args[0], "dry", StringComparison.Ordinal))
|
if (args.Length == 0 || !string.Equals(args[0], "dry", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Formats;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
|
||||||
|
namespace LightlessSyncServer.Services
|
||||||
|
{
|
||||||
|
public class ImageCheckService
|
||||||
|
{
|
||||||
|
private static readonly int _imageWidthAvatar = 512;
|
||||||
|
private static readonly int _imageHeightAvatar = 512;
|
||||||
|
private static readonly int _imageWidthBanner = 840;
|
||||||
|
private static readonly int _imageHeightBanner = 260;
|
||||||
|
private static readonly int _imageSize = 2000;
|
||||||
|
|
||||||
|
public class ImageLoadResult
|
||||||
|
{
|
||||||
|
public bool Success { get; init; }
|
||||||
|
public string? Base64Image { get; init; }
|
||||||
|
public string? ErrorMessage { get; init; }
|
||||||
|
|
||||||
|
public static ImageLoadResult Fail(string message) => new()
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ErrorMessage = message,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ImageLoadResult Ok(string base64) => new()
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Base64Image = base64,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static async Task<ImageLoadResult> ValidateImageAsync(string base64String, bool banner, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return ImageLoadResult.Fail("Operation cancelled.");
|
||||||
|
|
||||||
|
byte[] imageData;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
imageData = Convert.FromBase64String(base64String);
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
return ImageLoadResult.Fail("The provided image is not a valid Base64 string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Image<Rgba32>? image = null;
|
||||||
|
bool imageLoaded = false;
|
||||||
|
IImageFormat? format = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var ms = new MemoryStream(imageData))
|
||||||
|
{
|
||||||
|
format = await Image.DetectFormatAsync(ms, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format == null)
|
||||||
|
{
|
||||||
|
return ImageLoadResult.Fail("Unable to detect image format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
using (image = Image.Load<Rgba32>(imageData))
|
||||||
|
{
|
||||||
|
imageLoaded = true;
|
||||||
|
|
||||||
|
int maxWidth = banner ? _imageWidthBanner : _imageWidthAvatar;
|
||||||
|
int maxHeight = banner ? _imageHeightBanner : _imageHeightAvatar;
|
||||||
|
|
||||||
|
if (image.Width > maxWidth || image.Height > maxHeight)
|
||||||
|
{
|
||||||
|
var ratio = Math.Min((double)maxWidth / image.Width, (double)maxHeight / image.Height);
|
||||||
|
int newWidth = (int)(image.Width * ratio);
|
||||||
|
int newHeight = (int)(image.Height * ratio);
|
||||||
|
|
||||||
|
image.Mutate(x => x.Resize(newWidth, newHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
|
||||||
|
await image.SaveAsPngAsync(memoryStream, token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (memoryStream.Length > _imageSize * 1024)
|
||||||
|
{
|
||||||
|
return ImageLoadResult.Fail("Your image exceeds 2 MiB after resizing/conversion.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string base64Png = Convert.ToBase64String(memoryStream.GetBuffer(), 0, (int)memoryStream.Length);
|
||||||
|
return ImageLoadResult.Ok(base64Png);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
if (imageLoaded)
|
||||||
|
image?.Dispose();
|
||||||
|
|
||||||
|
return ImageLoadResult.Fail("Failed to load or process the image. It may be corrupted or unsupported.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using LightlessSync.API.Dto;
|
using LightlessSync.API.Dto;
|
||||||
using LightlessSync.API.SignalR;
|
using LightlessSync.API.SignalR;
|
||||||
using LightlessSyncServer.Hubs;
|
using LightlessSyncServer.Hubs;
|
||||||
|
using LightlessSyncServer.Models;
|
||||||
using LightlessSyncShared.Data;
|
using LightlessSyncShared.Data;
|
||||||
using LightlessSyncShared.Metrics;
|
using LightlessSyncShared.Metrics;
|
||||||
using LightlessSyncShared.Services;
|
using LightlessSyncShared.Services;
|
||||||
@@ -52,6 +53,13 @@ public sealed class SystemInfoService : BackgroundService
|
|||||||
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableIOWorkerThreads, ioThreads);
|
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableIOWorkerThreads, ioThreads);
|
||||||
|
|
||||||
var onlineUsers = (_redis.SearchKeysAsync("UID:*").GetAwaiter().GetResult()).Count();
|
var onlineUsers = (_redis.SearchKeysAsync("UID:*").GetAwaiter().GetResult()).Count();
|
||||||
|
|
||||||
|
var allLightfinderKeys = _redis.SearchKeysAsync("broadcast:*").GetAwaiter().GetResult().Where(c => !c.Contains("owner", StringComparison.Ordinal)).ToHashSet(StringComparer.Ordinal);
|
||||||
|
var allLightfinderItems = _redis.GetAllAsync<BroadcastRedisEntry>(allLightfinderKeys).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
var countLightFinderUsers = allLightfinderItems.Count;
|
||||||
|
var countLightFinderSyncshells = allLightfinderItems.Count(static l => !string.IsNullOrEmpty(l.Value.GID));
|
||||||
|
|
||||||
SystemInfoDto = new SystemInfoDto()
|
SystemInfoDto = new SystemInfoDto()
|
||||||
{
|
{
|
||||||
OnlineUsers = onlineUsers,
|
OnlineUsers = onlineUsers,
|
||||||
@@ -66,10 +74,12 @@ public sealed class SystemInfoService : BackgroundService
|
|||||||
using var db = await _dbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
|
using var db = await _dbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false);
|
||||||
|
|
||||||
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeAuthorizedConnections, onlineUsers);
|
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeAuthorizedConnections, onlineUsers);
|
||||||
|
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeLightFinderConnections, countLightFinderUsers);
|
||||||
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugePairs, db.ClientPairs.AsNoTracking().Count());
|
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugePairs, db.ClientPairs.AsNoTracking().Count());
|
||||||
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, db.Permissions.AsNoTracking().Where(p => p.IsPaused).Count());
|
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, db.Permissions.AsNoTracking().Count(p => p.IsPaused));
|
||||||
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeGroups, db.Groups.AsNoTracking().Count());
|
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeGroups, db.Groups.AsNoTracking().Count());
|
||||||
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeGroupPairs, db.GroupPairs.AsNoTracking().Count());
|
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeGroupPairs, db.GroupPairs.AsNoTracking().Count());
|
||||||
|
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeLightFinderGroups, countLightFinderSyncshells);
|
||||||
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeUsersRegistered, db.Users.AsNoTracking().Count());
|
_lightlessMetrics.SetGaugeTo(MetricsAPI.GaugeUsersRegistered, db.Users.AsNoTracking().Count());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -295,6 +295,8 @@ public class Startup
|
|||||||
}, new List<string>
|
}, new List<string>
|
||||||
{
|
{
|
||||||
MetricsAPI.GaugeAuthorizedConnections,
|
MetricsAPI.GaugeAuthorizedConnections,
|
||||||
|
MetricsAPI.GaugeLightFinderConnections,
|
||||||
|
MetricsAPI.GaugeLightFinderGroups,
|
||||||
MetricsAPI.GaugeConnections,
|
MetricsAPI.GaugeConnections,
|
||||||
MetricsAPI.GaugePairs,
|
MetricsAPI.GaugePairs,
|
||||||
MetricsAPI.GaugePairsPaused,
|
MetricsAPI.GaugePairsPaused,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.API.Data.Extensions;
|
using LightlessSync.API.Data.Extensions;
|
||||||
|
using LightlessSync.API.Dto.Group;
|
||||||
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSyncShared.Models;
|
using LightlessSyncShared.Models;
|
||||||
using static LightlessSyncServer.Hubs.LightlessHub;
|
using static LightlessSyncServer.Hubs.LightlessHub;
|
||||||
|
|
||||||
@@ -8,18 +10,95 @@ namespace LightlessSyncServer.Utils;
|
|||||||
|
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
|
public static void UpdateProfileFromDto(this GroupProfile profile, GroupProfileDto dto, string? base64PictureString = null, string? base64BannerString = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(profile);
|
||||||
|
ArgumentNullException.ThrowIfNull(dto);
|
||||||
|
|
||||||
|
if (profile == null || dto == null) return;
|
||||||
|
|
||||||
|
profile.Base64GroupProfileImage = string.IsNullOrWhiteSpace(base64PictureString) ? null : base64PictureString;
|
||||||
|
profile.Base64GroupBannerImage = string.IsNullOrWhiteSpace(base64BannerString) ? null : base64BannerString;
|
||||||
|
if (dto.Tags != null) profile.Tags = dto.Tags;
|
||||||
|
if (dto.Description != null) profile.Description = dto.Description;
|
||||||
|
if (dto.IsNsfw.HasValue) profile.IsNSFW = dto.IsNsfw.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void UpdateProfileFromDto(this UserProfileData profile, UserProfileDto dto, string? base64PictureString, string? base64BannerString = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(profile);
|
||||||
|
ArgumentNullException.ThrowIfNull(dto);
|
||||||
|
|
||||||
|
if (profile == null || dto == null) return;
|
||||||
|
|
||||||
|
profile.Base64ProfileImage = string.IsNullOrWhiteSpace(base64PictureString) ? null : base64PictureString;
|
||||||
|
profile.Base64BannerImage = string.IsNullOrWhiteSpace(base64BannerString) ? null : base64BannerString;
|
||||||
|
if (dto.Tags != null) profile.Tags = dto.Tags;
|
||||||
|
if (dto.Description != null) profile.UserDescription = dto.Description;
|
||||||
|
if (dto.IsNSFW.HasValue) profile.IsNSFW = dto.IsNSFW.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GroupProfileDto ToDTO(this GroupProfile groupProfile)
|
||||||
|
{
|
||||||
|
if (groupProfile == null)
|
||||||
|
{
|
||||||
|
return new GroupProfileDto(Group: null, Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: false, IsDisabled: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupData = groupProfile.Group?.ToGroupData();
|
||||||
|
|
||||||
|
return new GroupProfileDto(
|
||||||
|
groupData,
|
||||||
|
groupProfile.Description,
|
||||||
|
groupProfile.Tags,
|
||||||
|
groupProfile.Base64GroupProfileImage,
|
||||||
|
groupProfile.Base64GroupBannerImage,
|
||||||
|
groupProfile.IsNSFW,
|
||||||
|
groupProfile.ProfileDisabled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserProfileDto ToDTO(this UserProfileData userProfileData)
|
||||||
|
{
|
||||||
|
if (userProfileData == null)
|
||||||
|
{
|
||||||
|
return new UserProfileDto(User: null, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, Description: null, Tags: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
var userData = userProfileData.User?.ToUserData();
|
||||||
|
|
||||||
|
return new UserProfileDto(
|
||||||
|
userData,
|
||||||
|
userProfileData.ProfileDisabled,
|
||||||
|
userProfileData.IsNSFW,
|
||||||
|
userProfileData.Base64ProfileImage,
|
||||||
|
userProfileData.Base64BannerImage,
|
||||||
|
userProfileData.UserDescription,
|
||||||
|
userProfileData.Tags
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static GroupData ToGroupData(this Group group)
|
public static GroupData ToGroupData(this Group group)
|
||||||
{
|
{
|
||||||
|
if (group == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
return new GroupData(group.GID, group.Alias, group.CreatedDate);
|
return new GroupData(group.GID, group.Alias, group.CreatedDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UserData ToUserData(this GroupPair pair)
|
public static UserData ToUserData(this GroupPair pair)
|
||||||
{
|
{
|
||||||
|
if (pair == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
return new UserData(pair.GroupUser.UID, pair.GroupUser.Alias);
|
return new UserData(pair.GroupUser.UID, pair.GroupUser.Alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UserData ToUserData(this User user)
|
public static UserData ToUserData(this User user)
|
||||||
{
|
{
|
||||||
|
if (user == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
return new UserData(user.UID, user.Alias);
|
return new UserData(user.UID, user.Alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using LightlessSync.API.SignalR;
|
using LightlessSyncServer.Hubs;
|
||||||
using LightlessSyncServer.Hubs;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace LightlessSyncServer.Utils;
|
namespace LightlessSyncServer.Utils;
|
||||||
@@ -36,7 +36,9 @@
|
|||||||
"NotifyOwnerOnPairRequest": true,
|
"NotifyOwnerOnPairRequest": true,
|
||||||
"EnableBroadcasting": true,
|
"EnableBroadcasting": true,
|
||||||
"EnableSyncshellBroadcastPayloads": 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": "*",
|
"AllowedHosts": "*",
|
||||||
"Kestrel": {
|
"Kestrel": {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class LightlessMetrics
|
|||||||
if (!string.Equals(gauge, MetricsAPI.GaugeConnections, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(gauge, MetricsAPI.GaugeConnections, StringComparison.OrdinalIgnoreCase))
|
||||||
_gauges.Add(gauge, Prometheus.Metrics.CreateGauge(gauge, gauge));
|
_gauges.Add(gauge, Prometheus.Metrics.CreateGauge(gauge, gauge));
|
||||||
else
|
else
|
||||||
_gauges.Add(gauge, Prometheus.Metrics.CreateGauge(gauge, gauge, new[] { "continent" }));
|
_gauges.Add(gauge, Prometheus.Metrics.CreateGauge(gauge, gauge, ["continent", "country"]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ public class MetricsAPI
|
|||||||
public const string GaugeAvailableIOWorkerThreads = "lightless_available_threadpool_io";
|
public const string GaugeAvailableIOWorkerThreads = "lightless_available_threadpool_io";
|
||||||
public const string GaugeUsersRegistered = "lightless_users_registered";
|
public const string GaugeUsersRegistered = "lightless_users_registered";
|
||||||
public const string CounterUsersRegisteredDeleted = "lightless_users_registered_deleted";
|
public const string CounterUsersRegisteredDeleted = "lightless_users_registered_deleted";
|
||||||
|
public const string GaugeLightFinderConnections = "lightless_lightfinder_connections";
|
||||||
|
public const string GaugeLightFinderGroups = "lightless_lightfinder_groups";
|
||||||
public const string GaugePairs = "lightless_pairs";
|
public const string GaugePairs = "lightless_pairs";
|
||||||
public const string GaugePairsPaused = "lightless_pairs_paused";
|
public const string GaugePairsPaused = "lightless_pairs_paused";
|
||||||
public const string GaugeFilesTotal = "lightless_files";
|
public const string GaugeFilesTotal = "lightless_files";
|
||||||
|
|||||||
1177
LightlessSyncServer/LightlessSyncShared/Migrations/20251015173920_AddGroupDisabledAndNSFW.Designer.cs
generated
Normal file
1177
LightlessSyncServer/LightlessSyncShared/Migrations/20251015173920_AddGroupDisabledAndNSFW.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace LightlessSyncServer.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddGroupDisabledAndNSFW : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "is_nsfw",
|
||||||
|
table: "group_profiles",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "profile_disabled",
|
||||||
|
table: "group_profiles",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "is_nsfw",
|
||||||
|
table: "group_profiles");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "profile_disabled",
|
||||||
|
table: "group_profiles");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace LightlessSyncServer.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAndChangeTagsUserGroupProfile : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int[]>(
|
||||||
|
name: "tags",
|
||||||
|
table: "user_profile_data",
|
||||||
|
type: "integer[]",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
"ALTER TABLE group_profiles ALTER COLUMN tags TYPE integer[] USING string_to_array(tags, ',')::integer[];"
|
||||||
|
);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<int[]>(
|
||||||
|
name: "tags",
|
||||||
|
table: "group_profiles",
|
||||||
|
type: "integer[]",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "text",
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "tags",
|
||||||
|
table: "user_profile_data");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "tags",
|
||||||
|
table: "group_profiles",
|
||||||
|
type: "text",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(int[]),
|
||||||
|
oldType: "integer[]",
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1189
LightlessSyncServer/LightlessSyncShared/Migrations/20251020181150_AddBannerForProfiles.Designer.cs
generated
Normal file
1189
LightlessSyncServer/LightlessSyncShared/Migrations/20251020181150_AddBannerForProfiles.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace LightlessSyncServer.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddBannerForProfiles : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "base64banner_image",
|
||||||
|
table: "user_profile_data",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "base64group_banner_image",
|
||||||
|
table: "group_profiles",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "base64banner_image",
|
||||||
|
table: "user_profile_data");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "base64group_banner_image",
|
||||||
|
table: "group_profiles");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -589,6 +589,10 @@ namespace LightlessSyncServer.Migrations
|
|||||||
.HasColumnType("character varying(20)")
|
.HasColumnType("character varying(20)")
|
||||||
.HasColumnName("group_gid");
|
.HasColumnName("group_gid");
|
||||||
|
|
||||||
|
b.Property<string>("Base64GroupBannerImage")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("base64group_banner_image");
|
||||||
|
|
||||||
b.Property<string>("Base64GroupProfileImage")
|
b.Property<string>("Base64GroupProfileImage")
|
||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasColumnName("base64group_profile_image");
|
.HasColumnName("base64group_profile_image");
|
||||||
@@ -597,8 +601,16 @@ namespace LightlessSyncServer.Migrations
|
|||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasColumnName("description");
|
.HasColumnName("description");
|
||||||
|
|
||||||
b.Property<string>("Tags")
|
b.Property<bool>("IsNSFW")
|
||||||
.HasColumnType("text")
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("is_nsfw");
|
||||||
|
|
||||||
|
b.Property<bool>("ProfileDisabled")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasColumnName("profile_disabled");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<int[]>("Tags")
|
||||||
|
.HasColumnType("integer[]")
|
||||||
.HasColumnName("tags");
|
.HasColumnName("tags");
|
||||||
|
|
||||||
b.HasKey("GroupGID")
|
b.HasKey("GroupGID")
|
||||||
@@ -816,6 +828,10 @@ namespace LightlessSyncServer.Migrations
|
|||||||
.HasColumnType("character varying(10)")
|
.HasColumnType("character varying(10)")
|
||||||
.HasColumnName("user_uid");
|
.HasColumnName("user_uid");
|
||||||
|
|
||||||
|
b.Property<string>("Base64BannerImage")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("base64banner_image");
|
||||||
|
|
||||||
b.Property<string>("Base64ProfileImage")
|
b.Property<string>("Base64ProfileImage")
|
||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasColumnName("base64profile_image");
|
.HasColumnName("base64profile_image");
|
||||||
@@ -832,6 +848,10 @@ namespace LightlessSyncServer.Migrations
|
|||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
.HasColumnName("profile_disabled");
|
.HasColumnName("profile_disabled");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<int[]>("Tags")
|
||||||
|
.HasColumnType("integer[]")
|
||||||
|
.HasColumnName("tags");
|
||||||
|
|
||||||
b.Property<string>("UserDescription")
|
b.Property<string>("UserDescription")
|
||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasColumnName("user_description");
|
.HasColumnName("user_description");
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ public class GroupProfile
|
|||||||
public string GroupGID { get; set; }
|
public string GroupGID { get; set; }
|
||||||
public Group Group { get; set; }
|
public Group Group { get; set; }
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
public string Tags { get; set; }
|
public int[] Tags { get; set; }
|
||||||
public string Base64GroupProfileImage { get; set; }
|
public string Base64GroupProfileImage { get; set; }
|
||||||
|
public string Base64GroupBannerImage { get; set; }
|
||||||
|
public bool IsNSFW { get; set; } = false;
|
||||||
|
public bool ProfileDisabled { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ namespace LightlessSyncShared.Models;
|
|||||||
public class UserProfileData
|
public class UserProfileData
|
||||||
{
|
{
|
||||||
public string Base64ProfileImage { get; set; }
|
public string Base64ProfileImage { get; set; }
|
||||||
|
public string Base64BannerImage { get; set; }
|
||||||
public bool FlaggedForReport { get; set; }
|
public bool FlaggedForReport { get; set; }
|
||||||
public bool IsNSFW { get; set; }
|
public bool IsNSFW { get; set; }
|
||||||
public bool ProfileDisabled { get; set; }
|
public bool ProfileDisabled { get; set; }
|
||||||
public User User { get; set; }
|
public User User { get; set; }
|
||||||
|
|
||||||
public string UserDescription { get; set; }
|
public string UserDescription { get; set; }
|
||||||
|
public int[] Tags { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Key]
|
[Key]
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ public class StaticFilesServerConfiguration : LightlessConfigurationBase
|
|||||||
public string ColdStorageDirectory { get; set; } = null;
|
public string ColdStorageDirectory { get; set; } = null;
|
||||||
public double ColdStorageSizeHardLimitInGiB { get; set; } = -1;
|
public double ColdStorageSizeHardLimitInGiB { get; set; } = -1;
|
||||||
public int ColdStorageUnusedFileRetentionPeriodInDays { get; set; } = 30;
|
public int ColdStorageUnusedFileRetentionPeriodInDays { get; set; } = 30;
|
||||||
|
public bool EnableDirectDownloads { get; set; } = true;
|
||||||
|
public int DirectDownloadTokenLifetimeSeconds { get; set; } = 300;
|
||||||
[RemoteConfiguration]
|
[RemoteConfiguration]
|
||||||
public double SpeedTestHoursRateLimit { get; set; } = 0.5;
|
public double SpeedTestHoursRateLimit { get; set; } = 0.5;
|
||||||
[RemoteConfiguration]
|
[RemoteConfiguration]
|
||||||
@@ -40,6 +42,8 @@ public class StaticFilesServerConfiguration : LightlessConfigurationBase
|
|||||||
sb.AppendLine($"{nameof(CacheDirectory)} => {CacheDirectory}");
|
sb.AppendLine($"{nameof(CacheDirectory)} => {CacheDirectory}");
|
||||||
sb.AppendLine($"{nameof(DownloadQueueSize)} => {DownloadQueueSize}");
|
sb.AppendLine($"{nameof(DownloadQueueSize)} => {DownloadQueueSize}");
|
||||||
sb.AppendLine($"{nameof(DownloadQueueReleaseSeconds)} => {DownloadQueueReleaseSeconds}");
|
sb.AppendLine($"{nameof(DownloadQueueReleaseSeconds)} => {DownloadQueueReleaseSeconds}");
|
||||||
|
sb.AppendLine($"{nameof(EnableDirectDownloads)} => {EnableDirectDownloads}");
|
||||||
|
sb.AppendLine($"{nameof(DirectDownloadTokenLifetimeSeconds)} => {DirectDownloadTokenLifetimeSeconds}");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ public static class LightlessClaimTypes
|
|||||||
public const string Internal = "internal";
|
public const string Internal = "internal";
|
||||||
public const string Expires = "expiration_date";
|
public const string Expires = "expiration_date";
|
||||||
public const string Continent = "continent";
|
public const string Continent = "continent";
|
||||||
|
public const string Country = "country";
|
||||||
public const string DiscordUser = "discord_user";
|
public const string DiscordUser = "discord_user";
|
||||||
public const string DiscordId = "discord_user_id";
|
public const string DiscordId = "discord_user_id";
|
||||||
public const string OAuthLoginToken = "oauth_login_token";
|
public const string OAuthLoginToken = "oauth_login_token";
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using LightlessSyncShared.Services;
|
|||||||
using LightlessSyncShared.Utils.Configuration;
|
using LightlessSyncShared.Utils.Configuration;
|
||||||
using LightlessSyncStaticFilesServer.Services;
|
using LightlessSyncStaticFilesServer.Services;
|
||||||
using LightlessSyncStaticFilesServer.Utils;
|
using LightlessSyncStaticFilesServer.Utils;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -32,12 +33,15 @@ public class ServerFilesController : ControllerBase
|
|||||||
private readonly IDbContextFactory<LightlessDbContext> _lightlessDbContext;
|
private readonly IDbContextFactory<LightlessDbContext> _lightlessDbContext;
|
||||||
private readonly LightlessMetrics _metricsClient;
|
private readonly LightlessMetrics _metricsClient;
|
||||||
private readonly MainServerShardRegistrationService _shardRegistrationService;
|
private readonly MainServerShardRegistrationService _shardRegistrationService;
|
||||||
|
private readonly CDNDownloadUrlService _cdnDownloadUrlService;
|
||||||
|
private readonly CDNDownloadsService _cdnDownloadsService;
|
||||||
|
|
||||||
public ServerFilesController(ILogger<ServerFilesController> logger, CachedFileProvider cachedFileProvider,
|
public ServerFilesController(ILogger<ServerFilesController> logger, CachedFileProvider cachedFileProvider,
|
||||||
IConfigurationService<StaticFilesServerConfiguration> configuration,
|
IConfigurationService<StaticFilesServerConfiguration> configuration,
|
||||||
IHubContext<LightlessHub> hubContext,
|
IHubContext<LightlessHub> hubContext,
|
||||||
IDbContextFactory<LightlessDbContext> lightlessDbContext, LightlessMetrics metricsClient,
|
IDbContextFactory<LightlessDbContext> lightlessDbContext, LightlessMetrics metricsClient,
|
||||||
MainServerShardRegistrationService shardRegistrationService) : base(logger)
|
MainServerShardRegistrationService shardRegistrationService, CDNDownloadUrlService cdnDownloadUrlService,
|
||||||
|
CDNDownloadsService cdnDownloadsService) : base(logger)
|
||||||
{
|
{
|
||||||
_basePath = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false)
|
_basePath = configuration.GetValueOrDefault(nameof(StaticFilesServerConfiguration.UseColdStorage), false)
|
||||||
? configuration.GetValue<string>(nameof(StaticFilesServerConfiguration.ColdStorageDirectory))
|
? configuration.GetValue<string>(nameof(StaticFilesServerConfiguration.ColdStorageDirectory))
|
||||||
@@ -48,6 +52,8 @@ public class ServerFilesController : ControllerBase
|
|||||||
_lightlessDbContext = lightlessDbContext;
|
_lightlessDbContext = lightlessDbContext;
|
||||||
_metricsClient = metricsClient;
|
_metricsClient = metricsClient;
|
||||||
_shardRegistrationService = shardRegistrationService;
|
_shardRegistrationService = shardRegistrationService;
|
||||||
|
_cdnDownloadUrlService = cdnDownloadUrlService;
|
||||||
|
_cdnDownloadsService = cdnDownloadsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost(LightlessFiles.ServerFiles_DeleteAll)]
|
[HttpPost(LightlessFiles.ServerFiles_DeleteAll)]
|
||||||
@@ -105,6 +111,16 @@ public class ServerFilesController : ControllerBase
|
|||||||
baseUrl = shard.Value ?? _configuration.GetValue<Uri>(nameof(StaticFilesServerConfiguration.CdnFullUrl));
|
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
|
response.Add(new DownloadFileDto
|
||||||
{
|
{
|
||||||
FileExists = file.Size > 0,
|
FileExists = file.Size > 0,
|
||||||
@@ -113,6 +129,7 @@ public class ServerFilesController : ControllerBase
|
|||||||
Hash = file.Hash,
|
Hash = file.Hash,
|
||||||
Size = file.Size,
|
Size = file.Size,
|
||||||
Url = baseUrl?.ToString() ?? string.Empty,
|
Url = baseUrl?.ToString() ?? string.Empty,
|
||||||
|
CDNDownloadUrl = cdnDownloadUrl,
|
||||||
RawSize = file.RawSize
|
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()))));
|
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)]
|
[HttpPost(LightlessFiles.ServerFiles_FilesSend)]
|
||||||
public async Task<IActionResult> FilesSend([FromBody] FilesSendDto filesSendDto)
|
public async Task<IActionResult> FilesSend([FromBody] FilesSendDto filesSendDto)
|
||||||
{
|
{
|
||||||
@@ -360,4 +393,4 @@ public class ServerFilesController : ControllerBase
|
|||||||
buffer[i] ^= 42;
|
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<RequestFileStreamResultFactory>();
|
||||||
services.AddSingleton<ServerTokenGenerator>();
|
services.AddSingleton<ServerTokenGenerator>();
|
||||||
services.AddSingleton<RequestQueueService>();
|
services.AddSingleton<RequestQueueService>();
|
||||||
|
services.AddSingleton<CDNDownloadUrlService>();
|
||||||
|
services.AddSingleton<CDNDownloadsService>();
|
||||||
services.AddHostedService(p => p.GetService<RequestQueueService>());
|
services.AddHostedService(p => p.GetService<RequestQueueService>());
|
||||||
services.AddHostedService(m => m.GetService<FileStatisticsService>());
|
services.AddHostedService(m => m.GetService<FileStatisticsService>());
|
||||||
services.AddSingleton<IConfigurationService<LightlessConfigurationBase>, LightlessConfigurationServiceClient<LightlessConfigurationBase>>();
|
services.AddSingleton<IConfigurationService<LightlessConfigurationBase>, LightlessConfigurationServiceClient<LightlessConfigurationBase>>();
|
||||||
@@ -204,11 +206,12 @@ public class Startup
|
|||||||
}
|
}
|
||||||
else if (_isDistributionNode)
|
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
|
else
|
||||||
{
|
{
|
||||||
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(CacheController), typeof(RequestController), typeof(SpeedTestController)));
|
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(CacheController), typeof(ShardServerFilesController), typeof(RequestController), typeof(SpeedTestController)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -233,7 +236,6 @@ public class Startup
|
|||||||
}).AddJwtBearer();
|
}).AddJwtBearer();
|
||||||
services.AddAuthorization(options =>
|
services.AddAuthorization(options =>
|
||||||
{
|
{
|
||||||
options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
|
|
||||||
options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(LightlessClaimTypes.Internal, "true").Build());
|
options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(LightlessClaimTypes.Internal, "true").Build());
|
||||||
});
|
});
|
||||||
services.AddSingleton<IUserIdProvider, IdBasedUserIdProvider>();
|
services.AddSingleton<IUserIdProvider, IdBasedUserIdProvider>();
|
||||||
|
|||||||
@@ -25,7 +25,9 @@
|
|||||||
"UnusedFileRetentionPeriodInDays": 7,
|
"UnusedFileRetentionPeriodInDays": 7,
|
||||||
"CacheDirectory": "G:\\ServerTest",
|
"CacheDirectory": "G:\\ServerTest",
|
||||||
"ServiceAddress": "http://localhost:5002",
|
"ServiceAddress": "http://localhost:5002",
|
||||||
"RemoteCacheSourceUri": ""
|
"RemoteCacheSourceUri": "",
|
||||||
|
"EnableDirectDownloads": true,
|
||||||
|
"DirectDownloadTokenLifetimeSeconds": 300
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user