Compare commits

...

14 Commits

Author SHA1 Message Date
azyges
53f663fcbf zzz 2025-10-17 00:32:43 +09:00
azyges
47a94cb79f shivering my timbers 2025-10-17 00:20:26 +09:00
f933b40368 LightfinderMetrics
Reviewed-on: #14
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-10-16 00:12:56 +02:00
7e565ff85e CDN on shards
Reviewed-on: #17
2025-10-15 21:32:58 +02:00
defnotken
49177e639e conflicts 2025-10-15 14:15:30 -05:00
azyges
79483205f1 cdn download support on shards + clean up 2025-10-16 01:15:31 +09:00
CakeAndBanana
707c565ea9 Inverted boolean for syncshells 2025-10-15 04:02:27 +02:00
CakeAndBanana
6beda853f7 Added metric of broadcasted syncshells 2025-10-15 03:58:00 +02:00
CakeAndBanana
23dc6d7ef4 Merge branch 'metrics-lightfinder' of https://git.lightless-sync.org/Lightless-Sync/LightlessServer into metrics-lightfinder 2025-10-15 02:47:27 +02:00
CakeAndBanana
f686f7a6da Added lightfinder metric on startup 2025-10-15 02:47:18 +02:00
0fe1a43fb2 Merge branch 'master' into metrics-lightfinder 2025-10-14 18:56:37 +02:00
CakeAndBanana
43b9c6f90e Added lightfinder users in metrics 2025-10-14 18:45:57 +02:00
aadfaca629 .gitmodules updated
Specified branch to omit errors on pulling submodules
2025-10-13 00:07:54 +02:00
729d781fa3 Merge pull request 'Server - LightFinder Rework' (#13) from server-lightfinder-rewrite into master
Reviewed-on: #13
2025-10-12 15:14:21 +02:00
10 changed files with 261 additions and 21 deletions

1
.gitmodules vendored
View File

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

View File

@@ -72,7 +72,7 @@ public partial class LightlessHub
var existingData = await GetPairInfo(UserUID, otherUser.UID).ConfigureAwait(false); var existingData = await GetPairInfo(UserUID, otherUser.UID).ConfigureAwait(false);
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: RequestAbortedToken).ConfigureAwait(false); var ownDefaultPermissions = await DbContext.UserDefaultPreferredPermissions.AsNoTracking().SingleOrDefaultAsync(f => f.UserUID == UserUID, cancellationToken: RequestAbortedToken).ConfigureAwait(false);
@@ -523,6 +523,52 @@ public partial class LightlessHub
} }
} }
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) private static bool IsValidHashedCid(string? cid)
{ {
if (string.IsNullOrWhiteSpace(cid)) if (string.IsNullOrWhiteSpace(cid))
@@ -792,6 +838,97 @@ public partial class LightlessHub
return new UserProfileDto(user.User, false, data.IsNSFW, data.Base64ProfileImage, data.UserDescription); return new UserProfileDto(user.User, false, data.IsNSFW, data.Base64ProfileImage, data.UserDescription);
} }
[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, false, null, null, null);
}
if (profile.FlaggedForReport)
{
return new UserProfileDto(userData, true, null, null, "This profile is flagged for report and pending evaluation");
}
if (profile.ProfileDisabled)
{
return new UserProfileDto(userData, true, null, null, "This profile was permanently disabled");
}
return new UserProfileDto(userData, false, profile.IsNSFW, profile.Base64ProfileImage, profile.UserDescription);
}
[Authorize(Policy = "Identified")] [Authorize(Policy = "Identified")]
public async Task UserPushData(UserCharaDataMessageDto dto) public async Task UserPushData(UserCharaDataMessageDto dto)
{ {

View File

@@ -7,7 +7,9 @@ using LightlessSyncShared.Services;
using LightlessSyncShared.Utils.Configuration; using LightlessSyncShared.Utils.Configuration;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
using StackExchange.Redis.Extensions.Core.Abstractions; using StackExchange.Redis.Extensions.Core.Abstractions;
using static LightlessSyncServer.Hubs.LightlessHub;
namespace LightlessSyncServer.Services; namespace LightlessSyncServer.Services;
@@ -52,6 +54,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 +75,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());
} }

View File

@@ -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,

View File

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

View File

@@ -34,12 +34,14 @@ public class ServerFilesController : ControllerBase
private readonly LightlessMetrics _metricsClient; private readonly LightlessMetrics _metricsClient;
private readonly MainServerShardRegistrationService _shardRegistrationService; private readonly MainServerShardRegistrationService _shardRegistrationService;
private readonly CDNDownloadUrlService _cdnDownloadUrlService; 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, CDNDownloadUrlService cdnDownloadUrlService) : 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))
@@ -51,6 +53,7 @@ public class ServerFilesController : ControllerBase
_metricsClient = metricsClient; _metricsClient = metricsClient;
_shardRegistrationService = shardRegistrationService; _shardRegistrationService = shardRegistrationService;
_cdnDownloadUrlService = cdnDownloadUrlService; _cdnDownloadUrlService = cdnDownloadUrlService;
_cdnDownloadsService = cdnDownloadsService;
} }
[HttpPost(LightlessFiles.ServerFiles_DeleteAll)] [HttpPost(LightlessFiles.ServerFiles_DeleteAll)]
@@ -145,24 +148,16 @@ public class ServerFilesController : ControllerBase
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> DownloadFileDirect(string hash, [FromQuery] long expires, [FromQuery] string signature) public async Task<IActionResult> DownloadFileDirect(string hash, [FromQuery] long expires, [FromQuery] string signature)
{ {
if (!_cdnDownloadUrlService.DirectDownloadsEnabled) var result = await _cdnDownloadsService.GetDownloadAsync(hash, expires, signature).ConfigureAwait(false);
{
return NotFound();
}
hash = hash.ToUpperInvariant(); return result.Status switch
if (!_cdnDownloadUrlService.TryValidateSignature(hash, expires, signature))
{ {
return Unauthorized(); CDNDownloadsService.ResultStatus.Disabled => NotFound(),
} CDNDownloadsService.ResultStatus.Unauthorized => Unauthorized(),
CDNDownloadsService.ResultStatus.NotFound => NotFound(),
var fileInfo = await _cachedFileProvider.DownloadAndGetLocalFileInfo(hash).ConfigureAwait(false); CDNDownloadsService.ResultStatus.Success => PhysicalFile(result.File!.FullName, "application/octet-stream"),
if (fileInfo == null) _ => NotFound()
{ };
return NotFound();
}
return PhysicalFile(fileInfo.FullName, "application/octet-stream");
} }
[HttpPost(LightlessFiles.ServerFiles_FilesSend)] [HttpPost(LightlessFiles.ServerFiles_FilesSend)]

View File

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

View File

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

View File

@@ -88,6 +88,7 @@ public class Startup
services.AddSingleton<ServerTokenGenerator>(); services.AddSingleton<ServerTokenGenerator>();
services.AddSingleton<RequestQueueService>(); services.AddSingleton<RequestQueueService>();
services.AddSingleton<CDNDownloadUrlService>(); 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>>();
@@ -205,7 +206,8 @@ 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
{ {