mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 16:46:08 +02:00
Refactor and update various components of EchoHub.Server
- Updated launchSettings.json for consistency. - Refactored FileValidationHelper to improve image validation logic. - Enhanced ServerDirectoryService for better connection handling and user count updates. - Improved DatabaseSetup for legacy database handling and seeding default channels. - Refined FirstRunSetup to ensure JWT secret generation. - Removed appsettings.Development.json as it is no longer needed. - Updated EchoHub.Tests project file for consistency. - Added unit tests for FileValidationHelper and PresenceTracker with improved assertions. - Updated ValidationConstantsTests to ensure regex validations are correct. - Cleaned up solution file formatting for better readability.
This commit is contained in:
@@ -1,68 +1,68 @@
|
||||
namespace EchoHub.Server.Services;
|
||||
|
||||
public static class FileValidationHelper
|
||||
{
|
||||
private static readonly byte[] JpegMagic = [0xFF, 0xD8, 0xFF];
|
||||
private static readonly byte[] PngMagic = [0x89, 0x50, 0x4E, 0x47];
|
||||
private static readonly byte[] GifMagic = [0x47, 0x49, 0x46];
|
||||
private static readonly byte[] WebpRiff = [0x52, 0x49, 0x46, 0x46]; // "RIFF"
|
||||
private static readonly byte[] WebpTag = [0x57, 0x45, 0x42, 0x50]; // "WEBP"
|
||||
|
||||
/// <summary>
|
||||
/// Validates that a stream contains a recognized image format by checking magic bytes.
|
||||
/// The stream position is reset to the beginning after validation.
|
||||
/// </summary>
|
||||
public static bool IsValidImage(Stream stream)
|
||||
{
|
||||
if (!stream.CanSeek)
|
||||
return false;
|
||||
|
||||
var originalPosition = stream.Position;
|
||||
try
|
||||
{
|
||||
var header = new byte[12];
|
||||
var bytesRead = stream.Read(header, 0, header.Length);
|
||||
|
||||
if (bytesRead < 3)
|
||||
return false;
|
||||
|
||||
// JPEG: FF D8 FF
|
||||
if (StartsWith(header, bytesRead, JpegMagic))
|
||||
return true;
|
||||
|
||||
// PNG: 89 50 4E 47
|
||||
if (bytesRead >= 4 && StartsWith(header, bytesRead, PngMagic))
|
||||
return true;
|
||||
|
||||
// GIF: 47 49 46 (GIF87a or GIF89a)
|
||||
if (StartsWith(header, bytesRead, GifMagic))
|
||||
return true;
|
||||
|
||||
// WebP: RIFF....WEBP
|
||||
if (bytesRead >= 12 && StartsWith(header, bytesRead, WebpRiff)
|
||||
&& header[8] == WebpTag[0] && header[9] == WebpTag[1]
|
||||
&& header[10] == WebpTag[2] && header[11] == WebpTag[3])
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
stream.Position = originalPosition;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool StartsWith(byte[] buffer, int length, byte[] magic)
|
||||
{
|
||||
if (length < magic.Length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < magic.Length; i++)
|
||||
{
|
||||
if (buffer[i] != magic[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
namespace EchoHub.Server.Services;
|
||||
|
||||
public static class FileValidationHelper
|
||||
{
|
||||
private static readonly byte[] JpegMagic = [0xFF, 0xD8, 0xFF];
|
||||
private static readonly byte[] PngMagic = [0x89, 0x50, 0x4E, 0x47];
|
||||
private static readonly byte[] GifMagic = [0x47, 0x49, 0x46];
|
||||
private static readonly byte[] WebpRiff = [0x52, 0x49, 0x46, 0x46]; // "RIFF"
|
||||
private static readonly byte[] WebpTag = [0x57, 0x45, 0x42, 0x50]; // "WEBP"
|
||||
|
||||
/// <summary>
|
||||
/// Validates that a stream contains a recognized image format by checking magic bytes.
|
||||
/// The stream position is reset to the beginning after validation.
|
||||
/// </summary>
|
||||
public static bool IsValidImage(Stream stream)
|
||||
{
|
||||
if (!stream.CanSeek)
|
||||
return false;
|
||||
|
||||
var originalPosition = stream.Position;
|
||||
try
|
||||
{
|
||||
var header = new byte[12];
|
||||
var bytesRead = stream.Read(header, 0, header.Length);
|
||||
|
||||
if (bytesRead < 3)
|
||||
return false;
|
||||
|
||||
// JPEG: FF D8 FF
|
||||
if (StartsWith(header, bytesRead, JpegMagic))
|
||||
return true;
|
||||
|
||||
// PNG: 89 50 4E 47
|
||||
if (bytesRead >= 4 && StartsWith(header, bytesRead, PngMagic))
|
||||
return true;
|
||||
|
||||
// GIF: 47 49 46 (GIF87a or GIF89a)
|
||||
if (StartsWith(header, bytesRead, GifMagic))
|
||||
return true;
|
||||
|
||||
// WebP: RIFF....WEBP
|
||||
if (bytesRead >= 12 && StartsWith(header, bytesRead, WebpRiff)
|
||||
&& header[8] == WebpTag[0] && header[9] == WebpTag[1]
|
||||
&& header[10] == WebpTag[2] && header[11] == WebpTag[3])
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
stream.Position = originalPosition;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool StartsWith(byte[] buffer, int length, byte[] magic)
|
||||
{
|
||||
if (length < magic.Length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < magic.Length; i++)
|
||||
{
|
||||
if (buffer[i] != magic[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,137 +1,137 @@
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace EchoHub.Server.Services;
|
||||
|
||||
public sealed class ServerDirectoryService(
|
||||
IConfiguration configuration,
|
||||
PresenceTracker presenceTracker,
|
||||
ILogger<ServerDirectoryService> logger) : BackgroundService
|
||||
{
|
||||
private const string DirectoryHubUrl = "https://echohub.voidcube.cloud/hubs/servers";
|
||||
private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(30);
|
||||
|
||||
private HubConnection? _connection;
|
||||
private int _lastReportedUserCount = -1;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Yield to let the host finish starting before we log or connect
|
||||
await Task.Yield();
|
||||
|
||||
var isPublic = configuration.GetValue<bool>("Server:PublicServer");
|
||||
if (!isPublic)
|
||||
{
|
||||
logger.LogInformation("PublicServer is disabled — not registering with directory");
|
||||
return;
|
||||
}
|
||||
|
||||
var host = configuration["Server:PublicHost"];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
logger.LogWarning("PublicServer is enabled but Server:PublicHost is not set — skipping directory registration");
|
||||
return;
|
||||
}
|
||||
|
||||
var serverName = configuration["Server:Name"] ?? "EchoHub Server";
|
||||
var description = configuration["Server:Description"];
|
||||
|
||||
logger.LogInformation("PublicServer is enabled — connecting to EchoHubSpace directory as {Name} ({Host})", serverName, host);
|
||||
|
||||
_connection = new HubConnectionBuilder()
|
||||
.WithUrl(DirectoryHubUrl)
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
|
||||
_connection.Reconnected += async _ =>
|
||||
{
|
||||
logger.LogInformation("Reconnected to directory — re-registering server");
|
||||
await RegisterAsync(serverName, description, host);
|
||||
};
|
||||
|
||||
_connection.Closed += ex =>
|
||||
{
|
||||
if (ex is not null)
|
||||
logger.LogWarning(ex, "Directory connection closed with error");
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
// Initial connection with retry
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _connection.StartAsync(stoppingToken);
|
||||
logger.LogInformation("Successfully connected to EchoHubSpace API at {Url}", DirectoryHubUrl);
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to connect to directory — retrying in 30s");
|
||||
await Task.Delay(UpdateInterval, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (stoppingToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
// Register on first connect
|
||||
await RegisterAsync(serverName, description, host);
|
||||
|
||||
// Poll user count and send updates
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(UpdateInterval, stoppingToken);
|
||||
|
||||
if (_connection.State != HubConnectionState.Connected)
|
||||
continue;
|
||||
|
||||
var currentCount = presenceTracker.GetOnlineUserCount();
|
||||
if (currentCount == _lastReportedUserCount)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
await _connection.InvokeAsync("UpdateUserCount", currentCount, stoppingToken);
|
||||
_lastReportedUserCount = currentCount;
|
||||
logger.LogDebug("Updated directory user count to {Count}", currentCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to update user count on directory");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RegisterAsync(string name, string? description, string host)
|
||||
{
|
||||
if (_connection?.State != HubConnectionState.Connected)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var userCount = presenceTracker.GetOnlineUserCount();
|
||||
var dto = new RegisterServerDto(name, description, host, userCount);
|
||||
await _connection.InvokeAsync("RegisterServer", dto);
|
||||
_lastReportedUserCount = userCount;
|
||||
logger.LogInformation("Registered with directory as {Name} at {Host}", name, host);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to register with directory");
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_connection is not null)
|
||||
{
|
||||
await _connection.DisposeAsync();
|
||||
_connection = null;
|
||||
}
|
||||
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
internal record RegisterServerDto(string Name, string? Description, string Host, int UserCount);
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace EchoHub.Server.Services;
|
||||
|
||||
public sealed class ServerDirectoryService(
|
||||
IConfiguration configuration,
|
||||
PresenceTracker presenceTracker,
|
||||
ILogger<ServerDirectoryService> logger) : BackgroundService
|
||||
{
|
||||
private const string DirectoryHubUrl = "https://echohub.voidcube.cloud/hubs/servers";
|
||||
private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(30);
|
||||
|
||||
private HubConnection? _connection;
|
||||
private int _lastReportedUserCount = -1;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Yield to let the host finish starting before we log or connect
|
||||
await Task.Yield();
|
||||
|
||||
var isPublic = configuration.GetValue<bool>("Server:PublicServer");
|
||||
if (!isPublic)
|
||||
{
|
||||
logger.LogInformation("PublicServer is disabled — not registering with directory");
|
||||
return;
|
||||
}
|
||||
|
||||
var host = configuration["Server:PublicHost"];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
logger.LogWarning("PublicServer is enabled but Server:PublicHost is not set — skipping directory registration");
|
||||
return;
|
||||
}
|
||||
|
||||
var serverName = configuration["Server:Name"] ?? "EchoHub Server";
|
||||
var description = configuration["Server:Description"];
|
||||
|
||||
logger.LogInformation("PublicServer is enabled — connecting to EchoHubSpace directory as {Name} ({Host})", serverName, host);
|
||||
|
||||
_connection = new HubConnectionBuilder()
|
||||
.WithUrl(DirectoryHubUrl)
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
|
||||
_connection.Reconnected += async _ =>
|
||||
{
|
||||
logger.LogInformation("Reconnected to directory — re-registering server");
|
||||
await RegisterAsync(serverName, description, host);
|
||||
};
|
||||
|
||||
_connection.Closed += ex =>
|
||||
{
|
||||
if (ex is not null)
|
||||
logger.LogWarning(ex, "Directory connection closed with error");
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
// Initial connection with retry
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _connection.StartAsync(stoppingToken);
|
||||
logger.LogInformation("Successfully connected to EchoHubSpace API at {Url}", DirectoryHubUrl);
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to connect to directory — retrying in 30s");
|
||||
await Task.Delay(UpdateInterval, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (stoppingToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
// Register on first connect
|
||||
await RegisterAsync(serverName, description, host);
|
||||
|
||||
// Poll user count and send updates
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(UpdateInterval, stoppingToken);
|
||||
|
||||
if (_connection.State != HubConnectionState.Connected)
|
||||
continue;
|
||||
|
||||
var currentCount = presenceTracker.GetOnlineUserCount();
|
||||
if (currentCount == _lastReportedUserCount)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
await _connection.InvokeAsync("UpdateUserCount", currentCount, stoppingToken);
|
||||
_lastReportedUserCount = currentCount;
|
||||
logger.LogDebug("Updated directory user count to {Count}", currentCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to update user count on directory");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RegisterAsync(string name, string? description, string host)
|
||||
{
|
||||
if (_connection?.State != HubConnectionState.Connected)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var userCount = presenceTracker.GetOnlineUserCount();
|
||||
var dto = new RegisterServerDto(name, description, host, userCount);
|
||||
await _connection.InvokeAsync("RegisterServer", dto);
|
||||
_lastReportedUserCount = userCount;
|
||||
logger.LogInformation("Registered with directory as {Name} at {Host}", name, host);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to register with directory");
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_connection is not null)
|
||||
{
|
||||
await _connection.DisposeAsync();
|
||||
_connection = null;
|
||||
}
|
||||
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
internal record RegisterServerDto(string Name, string? Description, string Host, int UserCount);
|
||||
|
||||
Reference in New Issue
Block a user