lightfinder config, securing methods with stricter checking and added pair request notifications
This commit is contained in:
Submodule LightlessAPI updated: 5bfd21aaa9...69f0e310bd
@@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace LightlessSyncServer.Configuration;
|
||||||
|
|
||||||
|
public class BroadcastConfiguration : IBroadcastConfiguration
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan DefaultEntryTtl = TimeSpan.FromMinutes(5);
|
||||||
|
private const int DefaultMaxStatusBatchSize = 30;
|
||||||
|
private const string DefaultNotificationTemplate = "{DisplayName} sent you a pair request. To accept, right-click them, open the context menu, and send a request back.";
|
||||||
|
|
||||||
|
private readonly IOptionsMonitor<BroadcastOptions> _optionsMonitor;
|
||||||
|
|
||||||
|
public BroadcastConfiguration(IOptionsMonitor<BroadcastOptions> optionsMonitor)
|
||||||
|
{
|
||||||
|
_optionsMonitor = optionsMonitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BroadcastOptions Options => _optionsMonitor.CurrentValue ?? new BroadcastOptions();
|
||||||
|
|
||||||
|
public string RedisKeyPrefix
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var prefix = Options.RedisKeyPrefix;
|
||||||
|
return string.IsNullOrWhiteSpace(prefix) ? "broadcast:" : prefix!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan BroadcastEntryTtl
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var seconds = Options.EntryTtlSeconds;
|
||||||
|
return seconds > 0 ? TimeSpan.FromSeconds(seconds) : DefaultEntryTtl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int MaxStatusBatchSize
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var value = Options.MaxStatusBatchSize;
|
||||||
|
return value > 0 ? value : DefaultMaxStatusBatchSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool NotifyOwnerOnPairRequest => Options.NotifyOwnerOnPairRequest;
|
||||||
|
|
||||||
|
public bool EnableBroadcasting => Options.EnableBroadcasting;
|
||||||
|
|
||||||
|
public bool EnableSyncshellBroadcastPayloads => Options.EnableSyncshellBroadcastPayloads;
|
||||||
|
|
||||||
|
public string BuildRedisKey(string hashedCid)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(hashedCid))
|
||||||
|
return RedisKeyPrefix;
|
||||||
|
|
||||||
|
return string.Concat(RedisKeyPrefix, hashedCid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string BuildPairRequestNotification(string displayName)
|
||||||
|
{
|
||||||
|
var template = Options.PairRequestNotificationTemplate;
|
||||||
|
if (string.IsNullOrWhiteSpace(template))
|
||||||
|
{
|
||||||
|
template = DefaultNotificationTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayName = string.IsNullOrWhiteSpace(displayName) ? "Someone" : displayName;
|
||||||
|
|
||||||
|
return template.Replace("{DisplayName}", displayName, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace LightlessSyncServer.Configuration;
|
||||||
|
|
||||||
|
public class BroadcastOptions
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string RedisKeyPrefix { get; set; } = "broadcast:";
|
||||||
|
|
||||||
|
[Range(1, int.MaxValue)]
|
||||||
|
public int EntryTtlSeconds { get; set; } = 300;
|
||||||
|
|
||||||
|
[Range(1, int.MaxValue)]
|
||||||
|
public int MaxStatusBatchSize { get; set; } = 30;
|
||||||
|
|
||||||
|
public bool NotifyOwnerOnPairRequest { get; set; } = true;
|
||||||
|
|
||||||
|
public bool EnableBroadcasting { 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.";
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace LightlessSyncServer.Configuration;
|
||||||
|
|
||||||
|
public interface IBroadcastConfiguration
|
||||||
|
{
|
||||||
|
string RedisKeyPrefix { get; }
|
||||||
|
TimeSpan BroadcastEntryTtl { get; }
|
||||||
|
int MaxStatusBatchSize { get; }
|
||||||
|
bool NotifyOwnerOnPairRequest { get; }
|
||||||
|
bool EnableBroadcasting { get; }
|
||||||
|
bool EnableSyncshellBroadcastPayloads { get; }
|
||||||
|
|
||||||
|
string BuildRedisKey(string hashedCid);
|
||||||
|
string BuildPairRequestNotification(string displayName);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
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;
|
||||||
@@ -925,6 +925,13 @@ public partial class LightlessHub
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_broadcastConfiguration.EnableBroadcasting || !_broadcastConfiguration.EnableSyncshellBroadcastPayloads)
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("syncshell broadcast disabled", "User", UserUID, "GID", dto.GID));
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Syncshell broadcasting is currently disabled.").ConfigureAwait(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var (isOwner, _) = await TryValidateOwner(dto.GID).ConfigureAwait(false);
|
var (isOwner, _) = await TryValidateOwner(dto.GID).ConfigureAwait(false);
|
||||||
if (!isOwner)
|
if (!isOwner)
|
||||||
{
|
{
|
||||||
@@ -941,6 +948,9 @@ public partial class LightlessHub
|
|||||||
{
|
{
|
||||||
_logger.LogCallInfo(LightlessHubLogger.Args("Requested Syncshells", broadcastEntries.Select(b => b.GID)));
|
_logger.LogCallInfo(LightlessHubLogger.Args("Requested Syncshells", broadcastEntries.Select(b => b.GID)));
|
||||||
|
|
||||||
|
if (!_broadcastConfiguration.EnableBroadcasting || !_broadcastConfiguration.EnableSyncshellBroadcastPayloads)
|
||||||
|
return new List<GroupJoinDto>();
|
||||||
|
|
||||||
var results = new List<GroupJoinDto>();
|
var results = new List<GroupJoinDto>();
|
||||||
var gidsToValidate = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var gidsToValidate = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
@@ -949,10 +959,19 @@ public partial class LightlessHub
|
|||||||
if (string.IsNullOrWhiteSpace(entry.HashedCID) || string.IsNullOrWhiteSpace(entry.GID))
|
if (string.IsNullOrWhiteSpace(entry.HashedCID) || string.IsNullOrWhiteSpace(entry.GID))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var redisKey = $"broadcast:{entry.HashedCID}";
|
var redisKey = _broadcastConfiguration.BuildRedisKey(entry.HashedCID);
|
||||||
var redisEntry = await _redis.GetAsync<BroadcastRedisEntry>(redisKey).ConfigureAwait(false);
|
var redisEntry = await _redis.GetAsync<BroadcastRedisEntry>(redisKey).ConfigureAwait(false);
|
||||||
|
|
||||||
if (redisEntry?.GID != null && string.Equals(redisEntry.GID, entry.GID, StringComparison.OrdinalIgnoreCase))
|
if (redisEntry is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(redisEntry.HashedCID) && !string.Equals(redisEntry.HashedCID, entry.HashedCID, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("mismatched broadcast cid for group lookup", "Requested", entry.HashedCID, "EntryCID", redisEntry.HashedCID));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redisEntry.GID != null && string.Equals(redisEntry.GID, entry.GID, StringComparison.OrdinalIgnoreCase))
|
||||||
gidsToValidate.Add(entry.GID);
|
gidsToValidate.Add(entry.GID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,17 @@ 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.Utils;
|
using LightlessSyncServer.Utils;
|
||||||
|
using LightlessSyncServer.Configuration;
|
||||||
using LightlessSyncShared.Metrics;
|
using LightlessSyncShared.Metrics;
|
||||||
using LightlessSyncShared.Models;
|
using LightlessSyncShared.Models;
|
||||||
|
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 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;
|
||||||
@@ -252,9 +255,62 @@ 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(otherCid).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task NotifyBroadcastOwnerOfPairRequest(string targetHashedCid)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(targetHashedCid))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_broadcastConfiguration.EnableBroadcasting || !_broadcastConfiguration.NotifyOwnerOnPairRequest)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var db = _redis.Database;
|
||||||
|
var broadcastKey = _broadcastConfiguration.BuildRedisKey(targetHashedCid);
|
||||||
|
RedisValueWithExpiry broadcastValue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
broadcastValue = await db.StringGetWithExpiryAsync(broadcastKey).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("failed to fetch broadcast for pair notify", "CID", targetHashedCid, "Error", ex));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (broadcastValue.Value.IsNullOrEmpty || broadcastValue.Expiry is null || broadcastValue.Expiry <= TimeSpan.Zero)
|
||||||
|
return;
|
||||||
|
|
||||||
|
BroadcastRedisEntry? entry;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
entry = JsonSerializer.Deserialize<BroadcastRedisEntry>(broadcastValue.Value!);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("failed to deserialize broadcast for pair notify", "CID", targetHashedCid, "Value", broadcastValue.Value, "Error", ex));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry is null || !string.Equals(entry.HashedCID, targetHashedCid, StringComparison.Ordinal))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!entry.HasOwner())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (string.Equals(entry.OwnerUID, UserUID, StringComparison.Ordinal))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var senderAlias = Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, LightlessClaimTypes.Alias, StringComparison.Ordinal))?.Value;
|
||||||
|
var displayName = string.IsNullOrWhiteSpace(senderAlias) ? UserUID : senderAlias;
|
||||||
|
var message = _broadcastConfiguration.BuildPairRequestNotification(displayName);
|
||||||
|
|
||||||
|
await Clients.User(entry.OwnerUID).Client_ReceiveServerMessage(MessageSeverity.Information, message).ConfigureAwait(false);
|
||||||
|
}
|
||||||
private class PairingPayload
|
private class PairingPayload
|
||||||
{
|
{
|
||||||
public string UID { get; set; } = string.Empty;
|
public string UID { get; set; } = string.Empty;
|
||||||
@@ -264,14 +320,26 @@ public partial class LightlessHub
|
|||||||
public class BroadcastRedisEntry
|
public class BroadcastRedisEntry
|
||||||
{
|
{
|
||||||
public string HashedCID { get; set; } = string.Empty;
|
public string HashedCID { get; set; } = string.Empty;
|
||||||
|
public string OwnerUID { get; set; } = string.Empty;
|
||||||
public string? GID { get; set; }
|
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(string hashedCid, bool enabled, GroupBroadcastRequestDto? groupDto = null)
|
||||||
{
|
{
|
||||||
|
if (enabled && !_broadcastConfiguration.EnableBroadcasting)
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("broadcast disabled", UserUID, "CID", hashedCid));
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Broadcasting is currently disabled.").ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var db = _redis.Database;
|
var db = _redis.Database;
|
||||||
var broadcastKey = $"broadcast:{hashedCid}";
|
var broadcastKey = _broadcastConfiguration.BuildRedisKey(hashedCid);
|
||||||
|
|
||||||
if (enabled)
|
if (enabled)
|
||||||
{
|
{
|
||||||
@@ -279,6 +347,13 @@ public partial class LightlessHub
|
|||||||
|
|
||||||
if (groupDto is not null)
|
if (groupDto is not null)
|
||||||
{
|
{
|
||||||
|
if (!_broadcastConfiguration.EnableSyncshellBroadcastPayloads)
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("syncshell broadcast disabled", UserUID, "CID", hashedCid, "GID", groupDto.GID));
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Syncshell broadcasting is currently disabled.").ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
groupDto.HashedCID = hashedCid;
|
groupDto.HashedCID = hashedCid;
|
||||||
|
|
||||||
var valid = await SetGroupBroadcastStatus(groupDto).ConfigureAwait(false);
|
var valid = await SetGroupBroadcastStatus(groupDto).ConfigureAwait(false);
|
||||||
@@ -288,14 +363,36 @@ public partial class LightlessHub
|
|||||||
gid = groupDto.GID;
|
gid = groupDto.GID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BroadcastRedisEntry? existingEntry = null;
|
||||||
|
var existingValue = await db.StringGetAsync(broadcastKey).ConfigureAwait(false);
|
||||||
|
if (!existingValue.IsNullOrEmpty)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
existingEntry = JsonSerializer.Deserialize<BroadcastRedisEntry>(existingValue!);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("failed to deserialize broadcast entry during enable", "CID", hashedCid, "Value", existingValue, "Error", ex));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingEntry is not null && existingEntry.HasOwner() && !existingEntry.OwnedBy(UserUID))
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("unauthorized attempt to take broadcast ownership", UserUID, "CID", hashedCid, "ExistingOwner", existingEntry.OwnerUID));
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Another user is already broadcasting with that CID.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var entry = new BroadcastRedisEntry
|
var entry = new BroadcastRedisEntry
|
||||||
{
|
{
|
||||||
HashedCID = hashedCid,
|
HashedCID = hashedCid,
|
||||||
|
OwnerUID = UserUID,
|
||||||
GID = gid,
|
GID = gid,
|
||||||
};
|
};
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(entry);
|
var json = JsonSerializer.Serialize(entry);
|
||||||
await db.StringSetAsync(broadcastKey, json, TimeSpan.FromMinutes(5)).ConfigureAwait(false);
|
await db.StringSetAsync(broadcastKey, json, _broadcastConfiguration.BroadcastEntryTtl).ConfigureAwait(false);
|
||||||
_logger.LogCallInfo(LightlessHubLogger.Args("broadcast enabled", hashedCid, "GID", gid));
|
_logger.LogCallInfo(LightlessHubLogger.Args("broadcast enabled", hashedCid, "GID", gid));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -315,13 +412,20 @@ public partial class LightlessHub
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry is null || entry.HashedCID != hashedCid)
|
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");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entry.HasOwner() && !entry.OwnedBy(UserUID))
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("unauthorized attempt to remove broadcast", UserUID, "CID", hashedCid, "Owner", entry.OwnerUID));
|
||||||
|
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You can only disable your own broadcast. :3");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await db.KeyDeleteAsync(broadcastKey).ConfigureAwait(false);
|
await db.KeyDeleteAsync(broadcastKey).ConfigureAwait(false);
|
||||||
_logger.LogCallInfo(LightlessHubLogger.Args("broadcast disabled", hashedCid, "GID", entry.GID));
|
_logger.LogCallInfo(LightlessHubLogger.Args("broadcast disabled", hashedCid, "GID", entry.GID));
|
||||||
}
|
}
|
||||||
@@ -331,8 +435,11 @@ public partial class LightlessHub
|
|||||||
[Authorize(Policy = "Identified")]
|
[Authorize(Policy = "Identified")]
|
||||||
public async Task<BroadcastStatusInfoDto?> IsUserBroadcasting(string hashedCid)
|
public async Task<BroadcastStatusInfoDto?> IsUserBroadcasting(string hashedCid)
|
||||||
{
|
{
|
||||||
|
if (!_broadcastConfiguration.EnableBroadcasting)
|
||||||
|
return null;
|
||||||
|
|
||||||
var db = _redis.Database;
|
var db = _redis.Database;
|
||||||
var key = $"broadcast:{hashedCid}";
|
var key = _broadcastConfiguration.BuildRedisKey(hashedCid);
|
||||||
|
|
||||||
var result = await db.StringGetWithExpiryAsync(key).ConfigureAwait(false);
|
var result = await db.StringGetWithExpiryAsync(key).ConfigureAwait(false);
|
||||||
if (result.Expiry is null || result.Expiry <= TimeSpan.Zero || result.Value.IsNullOrEmpty)
|
if (result.Expiry is null || result.Expiry <= TimeSpan.Zero || result.Value.IsNullOrEmpty)
|
||||||
@@ -348,6 +455,12 @@ public partial class LightlessHub
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entry is not null && !string.Equals(entry.HashedCID, hashedCid, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("mismatched broadcast entry", "CID", hashedCid, "EntryCID", entry.HashedCID));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var dto = new BroadcastStatusInfoDto
|
var dto = new BroadcastStatusInfoDto
|
||||||
{
|
{
|
||||||
HashedCID = entry?.HashedCID ?? hashedCid,
|
HashedCID = entry?.HashedCID ?? hashedCid,
|
||||||
@@ -363,8 +476,11 @@ public partial class LightlessHub
|
|||||||
[Authorize(Policy = "Identified")]
|
[Authorize(Policy = "Identified")]
|
||||||
public async Task<TimeSpan?> GetBroadcastTtl(string hashedCid)
|
public async Task<TimeSpan?> GetBroadcastTtl(string hashedCid)
|
||||||
{
|
{
|
||||||
|
if (!_broadcastConfiguration.EnableBroadcasting)
|
||||||
|
return null;
|
||||||
|
|
||||||
var db = _redis.Database;
|
var db = _redis.Database;
|
||||||
var key = $"broadcast:{hashedCid}";
|
var key = _broadcastConfiguration.BuildRedisKey(hashedCid);
|
||||||
|
|
||||||
var value = await db.StringGetAsync(key).ConfigureAwait(false);
|
var value = await db.StringGetAsync(key).ConfigureAwait(false);
|
||||||
if (value.IsNullOrEmpty)
|
if (value.IsNullOrEmpty)
|
||||||
@@ -381,9 +497,21 @@ public partial class LightlessHub
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry?.HashedCID != hashedCid)
|
if (entry is null)
|
||||||
{
|
{
|
||||||
_logger.LogCallWarning(LightlessHubLogger.Args("unauthorized ttl query", UserUID, "CID", hashedCid, "EntryCID", entry?.HashedCID));
|
_logger.LogCallWarning(LightlessHubLogger.Args("missing broadcast entry during ttl query", "CID", hashedCid));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(entry.HashedCID, hashedCid, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("unauthorized ttl query", UserUID, "CID", hashedCid, "EntryCID", entry.HashedCID));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.HasOwner() && !entry.OwnedBy(UserUID))
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("unauthorized ttl query", UserUID, "CID", hashedCid, "Owner", entry.OwnerUID));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,19 +524,25 @@ public partial class LightlessHub
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private const int MaxBatchSize = 30;
|
|
||||||
|
|
||||||
[Authorize(Policy = "Identified")]
|
[Authorize(Policy = "Identified")]
|
||||||
public async Task<BroadcastStatusBatchDto> AreUsersBroadcasting(List<string> hashedCids)
|
public async Task<BroadcastStatusBatchDto?> AreUsersBroadcasting(List<string> hashedCids)
|
||||||
{
|
{
|
||||||
|
if (!_broadcastConfiguration.EnableBroadcasting)
|
||||||
|
{
|
||||||
|
_logger.LogCallInfo(LightlessHubLogger.Args("batch broadcast disabled", "Count", hashedCids.Count));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxBatchSize = _broadcastConfiguration.MaxStatusBatchSize;
|
||||||
|
if (hashedCids.Count > maxBatchSize)
|
||||||
|
hashedCids = hashedCids.Take(maxBatchSize).ToList();
|
||||||
|
|
||||||
var db = _redis.Database;
|
var db = _redis.Database;
|
||||||
if (hashedCids.Count > MaxBatchSize)
|
|
||||||
hashedCids = hashedCids.Take(MaxBatchSize).ToList();
|
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
var key = $"broadcast:{cid}";
|
var key = _broadcastConfiguration.BuildRedisKey(cid);
|
||||||
tasks[cid] = db.StringGetWithExpiryAsync(key);
|
tasks[cid] = db.StringGetWithExpiryAsync(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,7 +567,17 @@ public partial class LightlessHub
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
entry = JsonSerializer.Deserialize<BroadcastRedisEntry>(raw!);
|
entry = JsonSerializer.Deserialize<BroadcastRedisEntry>(raw!);
|
||||||
gid = entry?.GID;
|
if (entry is not null && !string.Equals(entry.HashedCID, cid, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_logger.LogCallWarning(LightlessHubLogger.Args("mismatched broadcast cid in batch", "Requested", cid, "EntryCID", entry.HashedCID));
|
||||||
|
entry = null;
|
||||||
|
gid = null;
|
||||||
|
isBroadcasting = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
gid = entry?.GID;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.API.Dto;
|
using LightlessSync.API.Dto;
|
||||||
using LightlessSync.API.SignalR;
|
using LightlessSync.API.SignalR;
|
||||||
using LightlessSyncServer.Services;
|
using LightlessSyncServer.Services;
|
||||||
|
using LightlessSyncServer.Configuration;
|
||||||
using LightlessSyncServer.Utils;
|
using LightlessSyncServer.Utils;
|
||||||
using LightlessSyncShared;
|
using LightlessSyncShared;
|
||||||
using LightlessSyncShared.Data;
|
using LightlessSyncShared.Data;
|
||||||
@@ -29,6 +30,7 @@ public partial class LightlessHub : Hub<ILightlessHub>, ILightlessHub
|
|||||||
private readonly LightlessHubLogger _logger;
|
private readonly LightlessHubLogger _logger;
|
||||||
private readonly string _shardName;
|
private readonly string _shardName;
|
||||||
private readonly int _maxExistingGroupsByUser;
|
private readonly int _maxExistingGroupsByUser;
|
||||||
|
private readonly IBroadcastConfiguration _broadcastConfiguration;
|
||||||
private readonly int _maxJoinedGroupsByUser;
|
private readonly int _maxJoinedGroupsByUser;
|
||||||
private readonly int _maxGroupUserCount;
|
private readonly int _maxGroupUserCount;
|
||||||
private readonly IRedisDatabase _redis;
|
private readonly IRedisDatabase _redis;
|
||||||
@@ -46,7 +48,7 @@ public partial class LightlessHub : Hub<ILightlessHub>, ILightlessHub
|
|||||||
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,
|
||||||
IRedisDatabase redisDb, OnlineSyncedPairCacheService onlineSyncedPairCacheService, LightlessCensus lightlessCensus,
|
IRedisDatabase redisDb, OnlineSyncedPairCacheService onlineSyncedPairCacheService, LightlessCensus lightlessCensus,
|
||||||
GPoseLobbyDistributionService gPoseLobbyDistributionService, PairService pairService)
|
GPoseLobbyDistributionService gPoseLobbyDistributionService, IBroadcastConfiguration broadcastConfiguration, PairService pairService)
|
||||||
{
|
{
|
||||||
_lightlessMetrics = lightlessMetrics;
|
_lightlessMetrics = lightlessMetrics;
|
||||||
_systemInfoService = systemInfoService;
|
_systemInfoService = systemInfoService;
|
||||||
@@ -65,6 +67,7 @@ public partial class LightlessHub : Hub<ILightlessHub>, ILightlessHub
|
|||||||
_gPoseLobbyDistributionService = gPoseLobbyDistributionService;
|
_gPoseLobbyDistributionService = gPoseLobbyDistributionService;
|
||||||
_logger = new LightlessHubLogger(this, logger);
|
_logger = new LightlessHubLogger(this, logger);
|
||||||
_dbContextLazy = new Lazy<LightlessDbContext>(() => lightlessDbContextFactory.CreateDbContext());
|
_dbContextLazy = new Lazy<LightlessDbContext>(() => lightlessDbContextFactory.CreateDbContext());
|
||||||
|
_broadcastConfiguration = broadcastConfiguration;
|
||||||
_pairService = pairService;
|
_pairService = pairService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using AspNetCoreRateLimit;
|
|||||||
using LightlessSync.API.SignalR;
|
using LightlessSync.API.SignalR;
|
||||||
using LightlessSyncAuthService.Controllers;
|
using LightlessSyncAuthService.Controllers;
|
||||||
using LightlessSyncServer.Controllers;
|
using LightlessSyncServer.Controllers;
|
||||||
|
using LightlessSyncServer.Configuration;
|
||||||
using LightlessSyncServer.Hubs;
|
using LightlessSyncServer.Hubs;
|
||||||
using LightlessSyncServer.Services;
|
using LightlessSyncServer.Services;
|
||||||
using LightlessSyncShared.Data;
|
using LightlessSyncShared.Data;
|
||||||
@@ -87,7 +88,9 @@ public class Startup
|
|||||||
|
|
||||||
services.Configure<ServerConfiguration>(Configuration.GetRequiredSection("LightlessSync"));
|
services.Configure<ServerConfiguration>(Configuration.GetRequiredSection("LightlessSync"));
|
||||||
services.Configure<LightlessConfigurationBase>(Configuration.GetRequiredSection("LightlessSync"));
|
services.Configure<LightlessConfigurationBase>(Configuration.GetRequiredSection("LightlessSync"));
|
||||||
|
services.Configure<BroadcastOptions>(Configuration.GetSection("Broadcast"));
|
||||||
|
|
||||||
|
services.AddSingleton<IBroadcastConfiguration, BroadcastConfiguration>();
|
||||||
services.AddSingleton<ServerTokenGenerator>();
|
services.AddSingleton<ServerTokenGenerator>();
|
||||||
services.AddSingleton<SystemInfoService>();
|
services.AddSingleton<SystemInfoService>();
|
||||||
services.AddSingleton<OnlineSyncedPairCacheService>();
|
services.AddSingleton<OnlineSyncedPairCacheService>();
|
||||||
|
|||||||
@@ -29,6 +29,15 @@
|
|||||||
"ServiceAddress": "http://localhost:5002",
|
"ServiceAddress": "http://localhost:5002",
|
||||||
"StaticFileServiceAddress": "http://localhost:5003"
|
"StaticFileServiceAddress": "http://localhost:5003"
|
||||||
},
|
},
|
||||||
|
"Broadcast": {
|
||||||
|
"RedisKeyPrefix": "broadcast:",
|
||||||
|
"EntryTtlSeconds": 10800,
|
||||||
|
"MaxStatusBatchSize": 30,
|
||||||
|
"NotifyOwnerOnPairRequest": true,
|
||||||
|
"EnableBroadcasting": true,
|
||||||
|
"EnableSyncshellBroadcastPayloads": true,
|
||||||
|
"PairRequestNotificationTemplate": "{DisplayName} sent you a pair request. To accept, right-click them, open the context menu, and send a request back."
|
||||||
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"Kestrel": {
|
"Kestrel": {
|
||||||
"Endpoints": {
|
"Endpoints": {
|
||||||
|
|||||||
Reference in New Issue
Block a user