Improve file formal and clear program self

This commit is contained in:
linmeideli committed 2026-08-29 16:28:01 +08:00
1 parent e1285972d7
commit 490a59be62
40 files changed
+1495 -511

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.
+39
View File
@@ -193,5 +193,44 @@
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/main/marketplace/Empostor.Plugins.PlayerStats.dll"
}
]
},
{
"id": "gg.empostor.playerlog",
"name": "Player Log",
"description": "Logs all player activity (chat, kills, meetings, joins, leaves) and provides an admin panel to view, search, and export logs.",
"author": "Empostor",
"versions": [
{
"version": "1.0.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/main/marketplace/Empostor.Plugins.PlayerLog.dll"
}
]
},
{
"id": "gg.empostor.monitor",
"name": "Monitor",
"description": "Provides /api/monitor/status and /api/monitor/health endpoints for server health checks and uptime monitoring.",
"author": "Empostor",
"versions": [
{
"version": "1.0.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/main/marketplace/Empostor.Plugins.Monitor.dll"
}
]
},
{
"id": "gg.empostor.privacy",
"name": "Privacy Policy",
"description": "Serves a customizable privacy policy page at /privacy with an admin panel editor.",
"author": "Empostor",
"versions": [
{
"version": "1.0.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/main/marketplace/Empostor.Plugins.Privacy.dll"
}
]
}
]
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>Empostor.Plugins.Monitor</AssemblyName>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Empostor.Api\Empostor.Api.csproj" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<Target Name="CopyToMarketplace" AfterTargets="Build">
<Copy SourceFiles="$(TargetPath)" DestinationFolder="..\..\marketplace" SkipUnchangedFiles="true" />
</Target>
</Project>
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
@@ -9,7 +8,7 @@ using Empostor.Api.Games.Managers;
using Empostor.Api.Net.Manager;
using Microsoft.AspNetCore.Mvc;
namespace Empostor.Server.Http;
namespace Empostor.Plugins.Monitor;
[ApiController]
public sealed class MonitorController : ControllerBase
@@ -0,0 +1,24 @@
using System.Threading.Tasks;
using Empostor.Api.Plugins;
using Microsoft.Extensions.Logging;
namespace Empostor.Plugins.Monitor;
[EmpostorPlugin("gg.empostor.monitor")]
public sealed class MonitorPlugin : PluginBase
{
private readonly ILogger<MonitorPlugin> _logger;
public MonitorPlugin(ILogger<MonitorPlugin> logger)
{
_logger = logger;
}
public override ValueTask EnableAsync()
{
_logger.LogInformation("[Monitor] Enabled. Status API at /api/monitor/status, health at /api/monitor/health");
return default;
}
public override ValueTask DisableAsync() => default;
}
@@ -0,0 +1,22 @@
using Empostor.Api.Plugins;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Empostor.Plugins.Monitor;
public sealed class MonitorPluginStartup : IPluginStartup, IPluginHttpStartup
{
public void ConfigureHost(IHostBuilder host) { }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddApplicationPart(typeof(MonitorPluginStartup).Assembly);
}
public void ConfigureWebApplication(IApplicationBuilder app)
{
}
}
@@ -0,0 +1,137 @@
{
"version": 1,
"dependencies": {
"net8.0": {
"BouncyCastle.Cryptography": {
"type": "Transitive",
"resolved": "2.6.1",
"contentHash": "vZsG2YILhthgRqO+ZVgRff4ZFKKTl0v7kqaVBLCtRvpREhfBP33pcWrdA3PRYgWuFL1RxiUFvjMUHTdBZlJcoA=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
}
},
"Microsoft.AspNetCore.Http.Features": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.2.0"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "9.0.6",
"contentHash": "0Zn6nR/6g+90MxskZyOOMPQvnPnrrGu6bytPwkV+azDcTtCSuQ1+GJUrg8Klmnrjk1i6zMpw2lXijl+tw7Q3kA=="
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"System.Diagnostics.DiagnosticSource": "8.0.0"
}
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.ObjectPool": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "X21b8D6z58EG34OMLH955750mFaEaGs5xLGe9Vyi3059Cgb3sk+p2k/JlQXaieEBRrk+SVlFzDei3Q6PdVzJOA=="
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
},
"Serilog": {
"type": "Transitive",
"resolved": "4.3.1-dev-02373",
"contentHash": "f0HNPaleOox5++8zqxAdi9afnUUkoznLOmd0ur/UnLbzl8bIaPyyBGOpbIJmC1kz2vHfkQ0fXh6KXMdlz7bcLQ=="
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ=="
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "Xg4G4Indi4dqP1iuAiMSwpiWS54ZghzR644OtsRCm/m/lBMG8dUBhLVN7hLm8NNrNTR+iGbshCPTwrvxZPlm4g=="
},
"empostor.api": {
"type": "Project",
"dependencies": {
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )",
"NextFast.Hazel": "[1.0.2-dev, )",
"NextFast.Hazel.Abstractions": "[1.0.2-dev, )"
}
},
"NextFast.Hazel": {
"type": "Project",
"dependencies": {
"BouncyCastle.Cryptography": "[2.6.1, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.6, )",
"Microsoft.Extensions.ObjectPool": "[8.0.1, )",
"NextFast.Hazel.Abstractions": "[1.0.2-dev, )",
"Serilog": "[4.3.1-dev-02373, )"
}
},
"NextFast.Hazel.Abstractions": {
"type": "Project"
}
}
}
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>Empostor.Plugins.PlayerLog</AssemblyName>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Empostor.Api\Empostor.Api.csproj" />
</ItemGroup>
<Target Name="CopyToMarketplace" AfterTargets="Build">
<Copy SourceFiles="$(TargetPath)" DestinationFolder="..\..\marketplace" SkipUnchangedFiles="true" />
</Target>
</Project>
@@ -0,0 +1,74 @@
using System.Linq;
using Empostor.Api.Admin;
namespace Empostor.Plugins.PlayerLog;
public sealed class PlayerLogAdminExtension : IAdminExtension
{
private readonly PlayerLogStore _store;
public PlayerLogAdminExtension(PlayerLogStore store)
{
_store = store;
}
public string Id => "player-log";
public string Title => "Player Log";
public string Icon => "list";
public string Section => "Server";
public void Build(AdminPanelBuilder b)
{
var recent = _store.GetAll().Take(50).ToList();
b.RegisterTable(t =>
{
t.Columns = new() { "Time", "Type", "Player", "Friend Code", "Game", "Detail" };
foreach (var e in recent)
{
t.Rows.Add(new()
{
new() { Text = e.Time.ToString("MM-dd HH:mm:ss") },
new() { Text = e.Type },
new() { Text = e.PlayerName ?? "—" },
new() { Text = e.FriendCode ?? "—" },
new() { Text = e.GameCode ?? "—" },
new() { Text = e.Detail ?? "—" },
});
}
});
b.RegisterText(txt =>
{
txt.Content = $"Total entries: {_store.GetAll().Count}";
txt.Tone = "muted";
});
b.RegisterButton(btn =>
{
btn.Label = "Export as JSON";
btn.Icon = "download";
btn.Style = "secondary";
btn.OnClick(async ctx =>
{
var data = _store.ExportJson();
return AdminActionResult.Ok($"Exported {data.Length} bytes. Download via /api/admin/player/logs/export");
});
});
b.RegisterButton(btn =>
{
btn.Label = "Clear All Logs";
btn.Icon = "trash";
btn.Style = "danger";
btn.OnClick(async ctx =>
{
_store.Clear();
return AdminActionResult.Ok("All player logs cleared.");
});
});
}
}
@@ -2,8 +2,9 @@ using Empostor.Api.Events;
using Empostor.Api.Events.Client;
using Empostor.Api.Events.Meeting;
using Empostor.Api.Events.Player;
using Empostor.Api.Innersloth;
namespace Empostor.Server.Service.Stat;
namespace Empostor.Plugins.PlayerLog;
internal sealed class PlayerLogListener : IEventListener
{
@@ -0,0 +1,24 @@
using System.Threading.Tasks;
using Empostor.Api.Plugins;
using Microsoft.Extensions.Logging;
namespace Empostor.Plugins.PlayerLog;
[EmpostorPlugin("gg.empostor.playerlog")]
public sealed class PlayerLogPlugin : PluginBase
{
private readonly ILogger<PlayerLogPlugin> _logger;
public PlayerLogPlugin(ILogger<PlayerLogPlugin> logger)
{
_logger = logger;
}
public override ValueTask EnableAsync()
{
_logger.LogInformation("[PlayerLog] Enabled. Player activity logs are available in the admin panel.");
return default;
}
public override ValueTask DisableAsync() => default;
}
@@ -0,0 +1,19 @@
using Empostor.Api.Admin;
using Empostor.Api.Events;
using Empostor.Api.Plugins;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Empostor.Plugins.PlayerLog;
public sealed class PlayerLogPluginStartup : IPluginStartup
{
public void ConfigureHost(IHostBuilder host) { }
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<PlayerLogStore>();
services.AddSingleton<IEventListener, PlayerLogListener>();
services.AddSingleton<IAdminExtension, PlayerLogAdminExtension>();
}
}
@@ -8,7 +8,7 @@ using System.Threading;
using Empostor.Api.Service;
using Microsoft.Extensions.Logging;
namespace Empostor.Server.Service.Stat;
namespace Empostor.Plugins.PlayerLog;
public sealed class PlayerLogEntry
{
@@ -77,32 +77,18 @@ public sealed class PlayerLogStore : JsonDataStore<List<PlayerLogEntry>>
.DefaultIfEmpty(0)
.Max();
/// <summary>
/// Returns the most recent non-empty player name recorded for a client.
/// Used by the admin panel so disconnected players still show their
/// original name instead of a generic "Disconnected" label.
/// </summary>
public string? GetLatestName(int clientId) =>
_entries.Where(e => e.ClientId == clientId)
.OrderByDescending(e => e.Time)
.Select(e => e.PlayerName)
.FirstOrDefault(n => !string.IsNullOrEmpty(n));
/// <summary>
/// Returns the most recent non-empty friend code recorded for a client.
/// </summary>
public string? GetLatestFriendCode(int clientId) =>
_entries.Where(e => e.ClientId == clientId)
.OrderByDescending(e => e.Time)
.Select(e => e.FriendCode)
.FirstOrDefault(f => !string.IsNullOrEmpty(f));
/// <summary>
/// Removes logged entries and persists the store.
/// When <paramref name="olderThan" /> is set, only entries strictly older
/// than the cutoff are removed (recent entries are kept); otherwise all
/// entries are removed.
/// </summary>
public void Clear(DateTime? olderThan = null)
{
if (olderThan.HasValue)
@@ -0,0 +1,495 @@
{
"version": 1,
"dependencies": {
"net8.0": {
"BouncyCastle.Cryptography": {
"type": "Transitive",
"resolved": "2.6.1",
"contentHash": "vZsG2YILhthgRqO+ZVgRff4ZFKKTl0v7kqaVBLCtRvpREhfBP33pcWrdA3PRYgWuFL1RxiUFvjMUHTdBZlJcoA=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
}
},
"Microsoft.AspNetCore.Http.Features": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.2.0"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.Configuration.CommandLine": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.Configuration.EnvironmentVariables": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "plvZ0ZIpq+97gdPNNvhwvrEZ92kNml9hd1pe3idMA7svR0PztdzVLkoWLcRFgySYXUJc3kSM3Xw3mNFMo/bxRA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.Configuration.FileExtensions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "McP+Lz/EKwvtCv48z0YImw+L1gi1gy5rHhNaNIY2CrjloV+XY8gydT8DjMR6zWeL13AFK+DioVpppwAuO1Gi1w==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
"Microsoft.Extensions.FileProviders.Physical": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Configuration.Json": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "C2wqUoh9OmRL1akaCcKSTmRU8z0kckfImG7zLNI8uyi47Lp+zd5LWAD17waPQEqCz3ioWOCrFUo+JJuoeZLOBw==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Configuration.FileExtensions": "8.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
"System.Text.Json": "8.0.0"
}
},
"Microsoft.Extensions.Configuration.UserSecrets": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Configuration.Json": "8.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
"Microsoft.Extensions.FileProviders.Physical": "8.0.0"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "9.0.6",
"contentHash": "0Zn6nR/6g+90MxskZyOOMPQvnPnrrGu6bytPwkV+azDcTtCSuQ1+GJUrg8Klmnrjk1i6zMpw2lXijl+tw7Q3kA=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "NSmDw3K0ozNDgShSIpsZcbFIzBX4w28nDag+TfaQujkXGazBm+lid5onlWoCBy4VsLxqnnKjEBbGSJVWJMf43g==",
"dependencies": {
"System.Text.Encodings.Web": "8.0.0",
"System.Text.Json": "8.0.0"
}
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"System.Diagnostics.DiagnosticSource": "8.0.0"
}
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.FileProviders.Physical": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "UboiXxpPUpwulHvIAVE36Knq0VSHaAmfrFkegLyBZeaADuKezJ/AIXYAW8F5GBlGk/VaibN2k/Zn1ca8YAfVdA==",
"dependencies": {
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
"Microsoft.Extensions.FileSystemGlobbing": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.FileSystemGlobbing": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ=="
},
"Microsoft.Extensions.Hosting": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
"Microsoft.Extensions.Configuration.CommandLine": "8.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0",
"Microsoft.Extensions.Configuration.FileExtensions": "8.0.0",
"Microsoft.Extensions.Configuration.Json": "8.0.0",
"Microsoft.Extensions.Configuration.UserSecrets": "8.0.0",
"Microsoft.Extensions.DependencyInjection": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Diagnostics": "8.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
"Microsoft.Extensions.FileProviders.Physical": "8.0.0",
"Microsoft.Extensions.Hosting.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging.Configuration": "8.0.0",
"Microsoft.Extensions.Logging.Console": "8.0.0",
"Microsoft.Extensions.Logging.Debug": "8.0.0",
"Microsoft.Extensions.Logging.EventLog": "8.0.0",
"Microsoft.Extensions.Logging.EventSource": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0"
}
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.Http": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Diagnostics": "8.0.0",
"Microsoft.Extensions.Logging": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.Logging.Configuration": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "ixXXV0G/12g6MXK65TLngYN9V5hQQRuV+fZi882WIoVJT7h5JvoYoxTEwCgdqwLjSneqh1O+66gM8sMr9z/rsQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0"
}
},
"Microsoft.Extensions.Logging.Console": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "e+48o7DztoYog+PY430lPxrM4mm3PbA6qucvQtUDDwVo4MO+ejMw7YGc/o2rnxbxj4isPxdfKFzTxvXMwAz83A==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging.Configuration": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"System.Text.Json": "8.0.0"
}
},
"Microsoft.Extensions.Logging.Debug": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "dt0x21qBdudHLW/bjMJpkixv858RRr8eSomgVbU8qljOyfrfDGi1JQvpF9w8S7ziRPtRKisuWaOwFxJM82GxeA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.Logging.EventLog": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"System.Diagnostics.EventLog": "8.0.0"
}
},
"Microsoft.Extensions.Logging.EventSource": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0",
"System.Text.Json": "8.0.0"
}
},
"Microsoft.Extensions.ObjectPool": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "X21b8D6z58EG34OMLH955750mFaEaGs5xLGe9Vyi3059Cgb3sk+p2k/JlQXaieEBRrk+SVlFzDei3Q6PdVzJOA=="
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
},
"Microsoft.IdentityModel.Abstractions": {
"type": "Transitive",
"resolved": "7.6.2",
"contentHash": "ULeyJwfYTMHOAArrBZorjPyM/BL5PFfwRzDtxlOxawO9vB/wVmHmbzZnOyHCOLJjel7XiVNmVnAs3H0jh4/9jQ=="
},
"Microsoft.IdentityModel.JsonWebTokens": {
"type": "Transitive",
"resolved": "7.6.2",
"contentHash": "ySbY72MsWw7BnSZw0fCsmp/oMou8ntJkfuZUBcplV3ncXMzxcXCn3fK6Gg2YLnTT6F2G9JXl9zc2lwfVfGOWEA==",
"dependencies": {
"Microsoft.IdentityModel.Tokens": "7.6.2"
}
},
"Microsoft.IdentityModel.Logging": {
"type": "Transitive",
"resolved": "7.6.2",
"contentHash": "0brV311MYxGz7Numa+pbVsxbz5tfe2nbAig1b5tQb3h/L1y5lkoPyOgD0qAfI0iX1njbwr8l9NdxIT1cDbzWKA==",
"dependencies": {
"Microsoft.IdentityModel.Abstractions": "7.6.2"
}
},
"Microsoft.IdentityModel.Tokens": {
"type": "Transitive",
"resolved": "7.6.2",
"contentHash": "pLnhCq9UNKWkn83zutkObYuzA+sOzx6VZpPI8hB8gD/vAXVt14D0SJ0sKPftwufvAbYGSNRda1vw/IFLbkjxNg==",
"dependencies": {
"Microsoft.IdentityModel.Logging": "7.6.2"
}
},
"Serilog": {
"type": "Transitive",
"resolved": "4.3.1-dev-02373",
"contentHash": "f0HNPaleOox5++8zqxAdi9afnUUkoznLOmd0ur/UnLbzl8bIaPyyBGOpbIJmC1kz2vHfkQ0fXh6KXMdlz7bcLQ=="
},
"Serilog.Extensions.Hosting": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "db0OcbWeSCvYQkHWu6n0v40N4kKaTAXNjlM3BKvcbwvNzYphQFcBR+36eQ/7hMMwOkJvAyLC2a9/jNdUL5NjtQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Hosting.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
"Serilog": "3.1.1",
"Serilog.Extensions.Logging": "8.0.0"
}
},
"Serilog.Extensions.Logging": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "YEAMWu1UnWgf1c1KP85l1SgXGfiVo0Rz6x08pCiPOIBt2Qe18tcZLvdBUuV5o1QHvrs8FAry9wTIhgBRtjIlEg==",
"dependencies": {
"Microsoft.Extensions.Logging": "8.0.0",
"Serilog": "3.1.1"
}
},
"Serilog.Settings.Configuration": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "nR0iL5HwKj5v6ULo3/zpP8NMcq9E2pxYA6XKTSWCbugVs4YqPyvaqaKOY+OMpPivKp7zMEpax2UKHnDodbRB0Q==",
"dependencies": {
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
"Microsoft.Extensions.DependencyModel": "8.0.0",
"Serilog": "3.1.1"
}
},
"Serilog.Sinks.Console": {
"type": "Transitive",
"resolved": "5.0.1",
"contentHash": "6Jt8jl9y2ey8VV7nVEUAyjjyxjAQuvd5+qj4XYAT9CwcsvR70HHULGBeD+K2WCALFXf7CFsNQT4lON6qXcu2AA==",
"dependencies": {
"Serilog": "3.1.1"
}
},
"Serilog.Sinks.File": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "lxjg89Y8gJMmFxVkbZ+qDgjl+T4yC5F7WSLTvA+5q0R04tfKVLRL/EHpYoJ/MEQd2EeCKDuylBIVnAYMotmh2A==",
"dependencies": {
"Serilog": "4.0.0"
}
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ=="
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A=="
},
"System.IdentityModel.Tokens.Jwt": {
"type": "Transitive",
"resolved": "7.6.2",
"contentHash": "dyvPIzxohovbECESKj9ep7yShujU5oN9dKYR3ivvoTM7nLANNrBC5jN9ysZfY5Hp/Xs0R1b79IsdHuu6D4xwow==",
"dependencies": {
"Microsoft.IdentityModel.JsonWebTokens": "7.6.2",
"Microsoft.IdentityModel.Tokens": "7.6.2"
}
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ=="
},
"System.Text.Json": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "OdrZO2WjkiEG6ajEFRABTRCi/wuXQPxeV6g8xvUJqdxMvvuCCEk86zPla8UiIQJz3durtUEbNyY/3lIhS0yZvQ==",
"dependencies": {
"System.Text.Encodings.Web": "8.0.0"
}
},
"empostor.api": {
"type": "Project",
"dependencies": {
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )",
"NextFast.Hazel": "[1.0.2-dev, )",
"NextFast.Hazel.Abstractions": "[1.0.2-dev, )"
}
},
"empostor.api.innersloth.generator": {
"type": "Project"
},
"empostor.server": {
"type": "Project",
"dependencies": {
"Empostor.Api": "[2.0.0-dev, )",
"Empostor.Api.Innersloth.Generator": "[2.0.0-dev, )",
"Microsoft.Extensions.FileSystemGlobbing": "[8.0.0, )",
"Microsoft.Extensions.Hosting": "[8.0.0, )",
"Microsoft.Extensions.Http": "[8.0.0, )",
"Microsoft.Extensions.ObjectPool": "[8.0.1, )",
"NextFast.Hazel": "[1.0.2-dev, )",
"NextFast.Hazel.Abstractions": "[1.0.2-dev, )",
"Serilog.Extensions.Hosting": "[8.0.0, )",
"Serilog.Settings.Configuration": "[8.0.0, )",
"Serilog.Sinks.Console": "[5.0.1, )",
"Serilog.Sinks.File": "[6.0.0, )",
"System.IdentityModel.Tokens.Jwt": "[7.6.2, )"
}
},
"NextFast.Hazel": {
"type": "Project",
"dependencies": {
"BouncyCastle.Cryptography": "[2.6.1, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.6, )",
"Microsoft.Extensions.ObjectPool": "[8.0.1, )",
"NextFast.Hazel.Abstractions": "[1.0.2-dev, )",
"Serilog": "[4.3.1-dev-02373, )"
}
},
"NextFast.Hazel.Abstractions": {
"type": "Project"
}
}
}
}
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>Empostor.Plugins.Privacy</AssemblyName>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Empostor.Api\Empostor.Api.csproj" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<Target Name="CopyToMarketplace" AfterTargets="Build">
<Copy SourceFiles="$(TargetPath)" DestinationFolder="..\..\marketplace" SkipUnchangedFiles="true" />
</Target>
</Project>
@@ -0,0 +1,44 @@
using System.Text.Json;
using Empostor.Api.Admin;
namespace Empostor.Plugins.Privacy;
public sealed class PrivacyAdminExtension : IAdminExtension
{
private readonly PrivacyStore _store;
public PrivacyAdminExtension(PrivacyStore store)
{
_store = store;
}
public string Id => "privacy-policy";
public string Title => "Privacy Policy";
public string Icon => "shield";
public string Section => "Server";
public void Build(AdminPanelBuilder b)
{
b.RegisterTextbox(t =>
{
t.Label = "Privacy Policy HTML";
t.Value = _store.GetContent();
t.Multiline = true;
t.SubmitOnChange = true;
t.OnSubmit(async ctx =>
{
_store.SaveContent(ctx.Value ?? string.Empty);
return AdminActionResult.Ok("Privacy policy saved.");
});
});
b.RegisterText(txt =>
{
txt.Content = "The privacy policy is served at /privacy. Edit the HTML above to update it.";
txt.Tone = "muted";
});
}
}
@@ -0,0 +1,60 @@
using System;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace Empostor.Plugins.Privacy;
[ApiController]
[Route("/privacy")]
public sealed class PrivacyController : ControllerBase
{
private readonly PrivacyStore _store;
private readonly ILogger<PrivacyController> _logger;
public PrivacyController(PrivacyStore store, ILogger<PrivacyController> logger)
{
_store = store;
_logger = logger;
}
[HttpGet]
public IActionResult GetPrivacy()
{
var html = _store.GetContent();
return Content(html, "text/html; charset=utf-8");
}
[HttpPost]
[Route("/admin/api/privacy")]
public async Task<IActionResult> UpdatePrivacy()
{
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync();
try
{
var doc = JsonDocument.Parse(body);
var content = doc.RootElement.GetProperty("content").GetString() ?? string.Empty;
var token = doc.RootElement.GetProperty("token").GetString() ?? string.Empty;
var adminToken = Environment.GetEnvironmentVariable("EMP_HTTP_TOKEN")
?? Environment.GetEnvironmentVariable("EMP_ADMIN_TOKEN")
?? "empostor";
if (token != adminToken)
{
return Unauthorized(new { error = "Invalid token." });
}
_store.SaveContent(content);
return Ok(new { success = true });
}
catch (Exception ex)
{
return BadRequest(new { error = ex.Message });
}
}
}
@@ -0,0 +1,24 @@
using System.Threading.Tasks;
using Empostor.Api.Plugins;
using Microsoft.Extensions.Logging;
namespace Empostor.Plugins.Privacy;
[EmpostorPlugin("gg.empostor.privacy")]
public sealed class PrivacyPlugin : PluginBase
{
private readonly ILogger<PrivacyPlugin> _logger;
public PrivacyPlugin(ILogger<PrivacyPlugin> logger)
{
_logger = logger;
}
public override ValueTask EnableAsync()
{
_logger.LogInformation("[Privacy] Enabled. Privacy policy page at /privacy");
return default;
}
public override ValueTask DisableAsync() => default;
}
@@ -0,0 +1,24 @@
using Empostor.Api.Admin;
using Empostor.Api.Plugins;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Empostor.Plugins.Privacy;
public sealed class PrivacyPluginStartup : IPluginStartup, IPluginHttpStartup
{
public void ConfigureHost(IHostBuilder host) { }
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<PrivacyStore>();
services.AddSingleton<IAdminExtension, PrivacyAdminExtension>();
services.AddControllers()
.AddApplicationPart(typeof(PrivacyPluginStartup).Assembly);
}
public void ConfigureWebApplication(IApplicationBuilder app)
{
}
}
@@ -1,70 +1,42 @@
using System;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Empostor.Api.Admin;
using Microsoft.Extensions.Logging;
namespace Empostor.Server.Http;
namespace Empostor.Plugins.Privacy;
[ApiController]
[Route("/privacy")]
public sealed class PrivacyController : ControllerBase
public sealed class PrivacyStore
{
private static readonly string PagesDir = Path.Combine(Directory.GetCurrentDirectory(), "Pages");
private static readonly string PrivacyFile = Path.Combine(PagesDir, "privacy.html");
private static bool _initialized;
private static readonly object _initLock = new();
private readonly ILogger<PrivacyController> _logger;
private readonly ILogger<PrivacyStore> _logger;
private bool _initialized;
private readonly object _initLock = new();
public PrivacyController(ILogger<PrivacyController> logger)
public PrivacyStore(ILogger<PrivacyStore> logger)
{
_logger = logger;
}
public string GetContent()
{
EnsureFile();
if (!File.Exists(PrivacyFile))
{
return DefaultPrivacyHtml();
}
return File.ReadAllText(PrivacyFile);
}
[HttpGet]
public IActionResult GetPrivacy()
public void SaveContent(string content)
{
if (!System.IO.File.Exists(PrivacyFile))
{
return Content(DefaultPrivacyHtml(), "text/html; charset=utf-8");
}
var html = System.IO.File.ReadAllText(PrivacyFile);
return Content(html, "text/html; charset=utf-8");
}
[HttpPost]
[Route("/admin/api/privacy")]
public async Task<IActionResult> UpdatePrivacy()
{
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync();
try
{
var doc = System.Text.Json.JsonDocument.Parse(body);
var content = doc.RootElement.GetProperty("content").GetString() ?? string.Empty;
var token = doc.RootElement.GetProperty("token").GetString() ?? string.Empty;
var adminToken = Environment.GetEnvironmentVariable("EMP_HTTP_TOKEN")
?? Environment.GetEnvironmentVariable("EMP_ADMIN_TOKEN")
?? "empostor";
if (token != adminToken)
{
return Unauthorized(new { error = "Invalid token." });
}
System.IO.File.WriteAllText(PrivacyFile, content);
_logger.LogInformation("PrivacyControllerPrivacy policy updated.");
return Ok(new { success = true });
}
catch (Exception ex)
{
return BadRequest(new { error = ex.Message });
}
EnsureFile();
File.WriteAllText(PrivacyFile, content);
_logger.LogInformation("Privacy policy updated.");
}
private void EnsureFile()
@@ -88,10 +60,10 @@ public sealed class PrivacyController : ControllerBase
Directory.CreateDirectory(PagesDir);
}
if (!System.IO.File.Exists(PrivacyFile))
if (!File.Exists(PrivacyFile))
{
System.IO.File.WriteAllText(PrivacyFile, DefaultPrivacyHtml());
_logger.LogInformation("PrivacyControllerWritten default Pages/privacy.html");
File.WriteAllText(PrivacyFile, DefaultPrivacyHtml());
_logger.LogInformation("Written default Pages/privacy.html");
}
}
}
@@ -0,0 +1,137 @@
{
"version": 1,
"dependencies": {
"net8.0": {
"BouncyCastle.Cryptography": {
"type": "Transitive",
"resolved": "2.6.1",
"contentHash": "vZsG2YILhthgRqO+ZVgRff4ZFKKTl0v7kqaVBLCtRvpREhfBP33pcWrdA3PRYgWuFL1RxiUFvjMUHTdBZlJcoA=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
}
},
"Microsoft.AspNetCore.Http.Features": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.2.0"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "9.0.6",
"contentHash": "0Zn6nR/6g+90MxskZyOOMPQvnPnrrGu6bytPwkV+azDcTtCSuQ1+GJUrg8Klmnrjk1i6zMpw2lXijl+tw7Q3kA=="
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"System.Diagnostics.DiagnosticSource": "8.0.0"
}
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.ObjectPool": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "X21b8D6z58EG34OMLH955750mFaEaGs5xLGe9Vyi3059Cgb3sk+p2k/JlQXaieEBRrk+SVlFzDei3Q6PdVzJOA=="
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
},
"Serilog": {
"type": "Transitive",
"resolved": "4.3.1-dev-02373",
"contentHash": "f0HNPaleOox5++8zqxAdi9afnUUkoznLOmd0ur/UnLbzl8bIaPyyBGOpbIJmC1kz2vHfkQ0fXh6KXMdlz7bcLQ=="
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ=="
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "Xg4G4Indi4dqP1iuAiMSwpiWS54ZghzR644OtsRCm/m/lBMG8dUBhLVN7hLm8NNrNTR+iGbshCPTwrvxZPlm4g=="
},
"empostor.api": {
"type": "Project",
"dependencies": {
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[8.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[8.0.0, )",
"NextFast.Hazel": "[1.0.2-dev, )",
"NextFast.Hazel.Abstractions": "[1.0.2-dev, )"
}
},
"NextFast.Hazel": {
"type": "Project",
"dependencies": {
"BouncyCastle.Cryptography": "[2.6.1, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[9.0.6, )",
"Microsoft.Extensions.ObjectPool": "[8.0.1, )",
"NextFast.Hazel.Abstractions": "[1.0.2-dev, )",
"Serilog": "[4.3.1-dev-02373, )"
}
},
"NextFast.Hazel.Abstractions": {
"type": "Project"
}
}
}
}
@@ -2,7 +2,7 @@ using System.Linq;
using System.Threading.Tasks;
using Empostor.Api.Commands;
using Empostor.Api.Net;
using Empostor.Server.Service.Admin.Ban;
using Empostor.Server.Service.Shared;
using Next.Hazel;
namespace Empostor.Server.Commands.Commands;
@@ -64,14 +64,14 @@ public sealed class BanCommand : ICommand
// Ban by friend code if available
if (!string.IsNullOrEmpty(fc))
{
_banStore.BanFriendCode(fc, reason);
_banStore.BanFriendCode(fc, reason, null);
}
// Ban by IP
var ip = target.Client.Connection?.EndPoint?.Address;
if (ip != null)
{
_banStore.BanIp(ip, reason);
_banStore.BanIp(ip, reason, null);
}
await target.Client.DisconnectAsync(DisconnectReason.Banned, reason);
+9 -115
View File
@@ -16,11 +16,8 @@ using Empostor.Api.Games.Managers;
using Empostor.Api.Net;
using Empostor.Api.Net.Manager;
using Empostor.Server.Http.Admin;
using Empostor.Server.Service.Admin.Ban;
using Empostor.Server.Service.Admin.Reactor;
using Empostor.Server.Service.Admin.Report;
using Empostor.Server.Service.Api;
using Empostor.Server.Service.Stat;
using Empostor.Server.Service.Shared;
using Empostor.Server.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -45,7 +42,6 @@ namespace Empostor.Server.Http
private readonly BanStore _bans;
private readonly AdminConfig _config;
private readonly ReportStore _reportStore;
private readonly PlayerLogStore _playerLogs;
private readonly HplpStore _hplpStore;
private readonly IOptions<ServerConfig> _serverConfig;
private readonly IOptions<HttpServerConfig> _httpServerConfig;
@@ -62,7 +58,6 @@ namespace Empostor.Server.Http
BanStore bans,
IOptions<AdminConfig> config,
ReportStore reportStore,
PlayerLogStore playerLogs,
HplpStore hplpStore,
IOptions<ServerConfig> serverConfig,
IOptions<HttpServerConfig> httpServerConfig,
@@ -76,7 +71,6 @@ namespace Empostor.Server.Http
_bans = bans;
_config = config.Value;
_reportStore = reportStore;
_playerLogs = playerLogs;
_hplpStore = hplpStore;
_serverConfig = serverConfig;
_httpServerConfig = httpServerConfig;
@@ -505,7 +499,7 @@ namespace Empostor.Server.Http
}
}
return Ok(new { banned = entry.Value, disconnected = kicked });
return Ok(new { banned = entry, disconnected = kicked });
}
[HttpPost("/api/admin/ban/fc")]
@@ -538,7 +532,7 @@ namespace Empostor.Server.Http
}
}
return Ok(new { banned = entry.Value, disconnected = kicked });
return Ok(new { banned = entry, disconnected = kicked });
}
[HttpPost("/api/admin/unban/ip")]
@@ -549,7 +543,12 @@ namespace Empostor.Server.Http
return Unauthorized();
}
return Ok(new { removed = _bans.UnbanIp(req.Value) });
if (!IPAddress.TryParse(req.Value, out var ip))
{
return BadRequest(Err("Invalid IP"));
}
return Ok(new { removed = _bans.UnbanIp(ip) });
}
[HttpPost("/api/admin/unban/fc")]
@@ -626,111 +625,6 @@ namespace Empostor.Server.Http
}));
}
[HttpGet("/api/admin/player/logs/clients")]
public IActionResult GetPlayerLogClients()
{
if (!IsAuthenticated())
{
return Unauthorized();
}
return Ok(_playerLogs.GetLoggedClientIds().Select(id => new
{
clientId = id,
name = FindClient(id)?.Name ?? _playerLogs.GetLatestName(id) ?? "Disconnected",
friendCode = FindClient(id)?.FriendCode ?? _playerLogs.GetLatestFriendCode(id) ?? "—",
}));
}
[HttpPost("/api/admin/player/logs/clear")]
public IActionResult ClearPlayerLogs([FromQuery] string? olderThan)
{
if (!IsAuthenticated())
{
return Unauthorized();
}
DateTime? cutoff = null;
if (!string.IsNullOrEmpty(olderThan)
&& !olderThan.Equals("all", StringComparison.OrdinalIgnoreCase))
{
cutoff = ParseLogCutoff(olderThan);
if (cutoff == null)
{
return BadRequest(new { error = "Invalid time range." });
}
}
_playerLogs.Clear(cutoff);
_logger.LogInformation("Admin cleared player logs (olderThan: {OlderThan})", olderThan ?? "all");
return Ok(new { cleared = true });
}
private static DateTime? ParseLogCutoff(string value)
{
var now = DateTime.UtcNow;
switch (value.Trim().ToLowerInvariant())
{
case "1h":
return now.AddHours(-1);
case "24h":
return now.AddDays(-1);
case "7d":
return now.AddDays(-7);
case "30d":
return now.AddDays(-30);
default:
return DateTime.TryParse(
value,
null,
System.Globalization.DateTimeStyles.AssumeUniversal
| System.Globalization.DateTimeStyles.AdjustToUniversal,
out var dt)
? dt.ToUniversalTime()
: (DateTime?)null;
}
}
[HttpGet("/api/admin/player/logs")]
public IActionResult GetPlayerLogs([FromQuery] int? clientId)
{
if (!IsAuthenticated())
{
return Unauthorized();
}
var logs = clientId.HasValue
? _playerLogs.GetByClient(clientId.Value)
: _playerLogs.GetAll();
return Ok(logs.Select(e => new
{
time = e.Time.ToString("yyyy-MM-dd HH:mm:ss"),
type = e.Type,
clientId = e.ClientId,
playerName = e.PlayerName ?? "—",
friendCode = e.FriendCode ?? "—",
gameCode = e.GameCode ?? "—",
detail = e.Detail ?? "—",
}));
}
[HttpGet("/api/admin/player/logs/export")]
public IActionResult ExportPlayerLogs([FromQuery] int? clientId)
{
if (!IsAuthenticated())
{
return Unauthorized();
}
var data = clientId.HasValue
? _playerLogs.ExportJson(clientId.Value)
: _playerLogs.ExportJson();
var name = clientId.HasValue
? $"player_{clientId}_logs.json"
: "all_player_logs.json";
return File(data, "application/json; charset=utf-8", name);
}
[HttpGet("/api/admin/hplp")]
public IActionResult GetHplp()
{
+1 -1
View File
@@ -4,7 +4,7 @@ using Empostor.Api.Config;
using Empostor.Api.Games;
using Empostor.Api.Games.Managers;
using Empostor.Api.Innersloth;
using Empostor.Server.Service.Admin.Reactor;
using Empostor.Server.Service.Shared;
using Empostor.Server.Service.Api;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+1 -1
View File
@@ -13,7 +13,7 @@ using Empostor.Api.Net.Messages.C2S;
using Empostor.Api.Net.Messages.S2C;
using Empostor.Server.Events.Player;
using Empostor.Server.Net.Manager;
using Empostor.Server.Service.Admin.Report;
using Empostor.Server.Service.Shared;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Next.Hazel;
@@ -14,9 +14,10 @@ using Empostor.Api.Service;
using Empostor.Server.Events.Client;
using Empostor.Server.Net.Factories;
using Empostor.Server.Service;
using Empostor.Server.Service.Admin.Reactor;
using Empostor.Server.Service.Shared;
using Empostor.Server.Service.Auth;
using Empostor.Server.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Next.Hazel;
@@ -6,7 +6,7 @@ using Empostor.Api.Games;
using Empostor.Api.Net;
using Next.Hazel;
using Empostor.Server.Events;
using Empostor.Server.Service.Admin.Reactor;
using Empostor.Server.Service.Shared;
using Empostor.Server.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+1 -6
View File
@@ -25,14 +25,11 @@ using Empostor.Server.Net.Manager;
using Empostor.Server.Net.Messages;
using Empostor.Server.Plugins;
using Empostor.Server.Recorder;
using Empostor.Server.Service.Admin.Ban;
using Empostor.Server.Service.Admin.Reactor;
using Empostor.Server.Service.Admin.Report;
using Empostor.Server.Service.Shared;
using Empostor.Server.Service.Api;
using Empostor.Api.Service;
using Empostor.Server.Service.Auth;
using Empostor.Server.Service.Firewall;
using Empostor.Server.Service.Stat;
using Empostor.Server.Utils;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@@ -199,8 +196,6 @@ namespace Empostor.Server
services.AddSingleton<ReportStore>();
services.AddSingleton<IEventListener, BanEnforcementListener>();
services.AddSingleton<IEventListener, ReactorHandshakeListener>();
services.AddSingleton<PlayerLogStore>();
services.AddSingleton<IEventListener, PlayerLogListener>();
services.AddSingleton<BanCommand>();
services.AddSingleton<HplpStore>();
@@ -6,7 +6,7 @@ using Empostor.Api.Net.Messages;
using Empostor.Server.Net;
using Empostor.Server.Net.Hazel;
using Empostor.Server.Net.Manager;
using Empostor.Server.Service.Admin.Report;
using Empostor.Server.Service.Shared;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -1,244 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Empostor.Api.Service;
using Microsoft.Extensions.Logging;
namespace Empostor.Server.Service.Admin.Ban
{
public sealed class BanStore : JsonDataStore<BanStore.BanData>
{
private ConcurrentDictionary<string, BanEntry> _ips = new();
private ConcurrentDictionary<string, BanEntry> _friendCodes = new();
public BanStore(ILogger<BanStore> logger)
: base(logger, legacyPath: "bans.json")
{
Load();
}
public bool IsIpBanned(IPAddress ip) => GetIpBan(ip) != null;
public bool IsFriendCodeBanned(string? fc) => GetFriendCodeBan(fc) != null;
public BanEntry? GetIpBan(IPAddress ip)
{
var key = Normalize(ip);
if (!_ips.TryGetValue(key, out var entry))
{
return null;
}
if (!entry.IsExpired)
{
return entry;
}
_ips.TryRemove(key, out _);
SaveFireAndForget();
return null;
}
public BanEntry? GetFriendCodeBan(string? fc)
{
if (fc == null || !_friendCodes.TryGetValue(fc, out var entry))
{
return null;
}
if (!entry.IsExpired)
{
return entry;
}
_friendCodes.TryRemove(fc, out _);
SaveFireAndForget();
return null;
}
public BanEntry BanIp(IPAddress ip, string reason, DateTime? bannedUntil = null)
{
var key = Normalize(ip);
var entry = new BanEntry { Value = key, Reason = reason, BannedAt = DateTime.UtcNow, BannedUntil = bannedUntil };
_ips[key] = entry;
SaveFireAndForget();
return entry;
}
public bool UnbanIp(string key)
{
var r = _ips.TryRemove(key, out _);
if (r)
{
SaveFireAndForget();
}
return r;
}
public BanEntry BanFriendCode(string fc, string reason, DateTime? bannedUntil = null)
{
var entry = new BanEntry { Value = fc, Reason = reason, BannedAt = DateTime.UtcNow, BannedUntil = bannedUntil };
_friendCodes[fc] = entry;
SaveFireAndForget();
return entry;
}
public bool UnbanFriendCode(string key)
{
var r = _friendCodes.TryRemove(key, out _);
if (r)
{
SaveFireAndForget();
}
return r;
}
public IReadOnlyList<BanEntry> AllIpBans()
{
if (PruneExpired())
{
SaveFireAndForget();
}
return _ips.Values.OrderByDescending(b => b.BannedAt).ToList();
}
public IReadOnlyList<BanEntry> AllFriendCodeBans()
{
if (PruneExpired())
{
SaveFireAndForget();
}
return _friendCodes.Values.OrderByDescending(b => b.BannedAt).ToList();
}
public (int IpCount, int FcCount) Stats()
{
if (PruneExpired())
{
SaveFireAndForget();
}
return (_ips.Count, _friendCodes.Count);
}
/// <summary>
/// Parses a ban duration string into a UTC expiry timestamp.
/// Supported formats: "permanent"/"forever" (or empty) for a permanent ban,
/// or "&lt;number&gt;&lt;unit&gt;" where unit is h (hours), d (days), mo (months) or y (years).
/// Returns null for a permanent ban.
/// </summary>
public static DateTime? ParseDuration(string? duration)
{
if (string.IsNullOrWhiteSpace(duration))
{
return null;
}
var value = duration.Trim();
if (value.Equals("permanent", StringComparison.OrdinalIgnoreCase)
|| value.Equals("forever", StringComparison.OrdinalIgnoreCase)
|| value.Equals("perm", StringComparison.OrdinalIgnoreCase))
{
return null;
}
var match = Regex.Match(value, @"^(\d+)(h|d|mo|y)$", RegexOptions.IgnoreCase);
if (!match.Success)
{
return null;
}
var amount = int.Parse(match.Groups[1].Value);
var unit = match.Groups[2].Value.ToLowerInvariant();
var now = DateTime.UtcNow;
return unit switch
{
"h" => now.AddHours(amount),
"d" => now.AddDays(amount),
"mo" => now.AddMonths(amount),
"y" => now.AddYears(amount),
_ => null,
};
}
protected override BanData GetSnapshot()
{
PruneExpired();
return new()
{
Ips = new(_ips),
FriendCodes = new(_friendCodes),
};
}
protected override void ApplySnapshot(BanData data)
{
_ips = new(data.Ips ?? new());
_friendCodes = new(data.FriendCodes ?? new());
}
private bool PruneExpired()
{
var changed = false;
foreach (var (key, entry) in _ips)
{
if (entry.IsExpired && _ips.TryRemove(key, out _))
{
changed = true;
}
}
foreach (var (key, entry) in _friendCodes)
{
if (entry.IsExpired && _friendCodes.TryRemove(key, out _))
{
changed = true;
}
}
return changed;
}
private static string Normalize(IPAddress ip)
=> ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4().ToString() : ip.ToString();
public sealed class BanData
{
[JsonPropertyName("ips")]
public Dictionary<string, BanEntry>? Ips { get; set; }
[JsonPropertyName("friendCodes")]
public Dictionary<string, BanEntry>? FriendCodes { get; set; }
}
}
public sealed class BanEntry
{
[JsonPropertyName("value")]
public required string Value { get; init; }
[JsonPropertyName("reason")]
public required string Reason { get; init; }
[JsonPropertyName("bannedAt")]
public required DateTime BannedAt { get; init; }
[JsonPropertyName("bannedUntil")]
public DateTime? BannedUntil { get; init; }
[JsonIgnore]
public bool IsExpired => BannedUntil.HasValue && BannedUntil.Value <= DateTime.UtcNow;
[JsonIgnore]
public bool IsPermanent => !BannedUntil.HasValue;
}
}
+6 -4
View File
@@ -1,5 +1,4 @@
using Empostor.Api.Service;
using Empostor.Server.Service.Stat;
using Microsoft.Extensions.Logging;
namespace Empostor.Server.Service;
@@ -16,14 +15,17 @@ public sealed class ClientIdStore : JsonDataStore<long>
{
private long _lastId;
public ClientIdStore(ILogger<ClientIdStore> logger, PlayerLogStore playerLogs)
public ClientIdStore(ILogger<ClientIdStore> logger)
: base(logger)
{
Load();
}
_lastId = System.Math.Max(_lastId, playerLogs.GetMaxClientId());
if (_lastId > 0)
public void InitializeWithMaxId(long maxId)
{
if (maxId > _lastId)
{
_lastId = maxId;
SaveFireAndForget();
}
}
@@ -5,7 +5,7 @@ using Empostor.Api.Events.Client;
using Empostor.Api.Languages;
using Microsoft.Extensions.Logging;
namespace Empostor.Server.Service.Admin.Ban;
namespace Empostor.Server.Service.Shared;
public sealed class BanEnforcementListener : IEventListener
{
@@ -26,7 +26,7 @@ public sealed class BanEnforcementListener : IEventListener
var client = e.Client;
var ip = client.Connection?.EndPoint?.Address;
// IP 封禁检查
// IP ban check
if (ip != null && _bans.GetIpBan(ip) is { } ipBan)
{
_logger.LogWarning("BanRejecting banned IP {Ip} ({Name})", ip, client.Name);
@@ -34,7 +34,7 @@ public sealed class BanEnforcementListener : IEventListener
return;
}
// FriendCode 封禁检查
// FriendCode ban check
if (_bans.GetFriendCodeBan(client.FriendCode) is { } fcBan)
{
_logger.LogWarning("BanRejecting banned FriendCode {FC} ({Name})", client.FriendCode, client.Name);
@@ -0,0 +1,205 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Empostor.Api.Service;
using Microsoft.Extensions.Logging;
namespace Empostor.Server.Service.Shared
{
public sealed class BanStore : JsonDataStore<BanStore.BanData>
{
private ConcurrentDictionary<string, BanEntry> _ips = new();
private ConcurrentDictionary<string, BanEntry> _friendCodes = new();
public BanStore(ILogger<BanStore> logger)
: base(logger, legacyPath: "bans.json")
{
Load();
}
public bool IsIpBanned(IPAddress ip) => GetIpBan(ip) != null;
public bool IsFriendCodeBanned(string? fc) => GetFriendCodeBan(fc) != null;
public BanEntry? GetIpBan(IPAddress ip)
{
var key = Normalize(ip);
if (!_ips.TryGetValue(key, out var entry))
{
return null;
}
if (!entry.IsExpired)
{
return entry;
}
_ips.TryRemove(key, out _);
SaveFireAndForget();
return null;
}
public BanEntry? GetFriendCodeBan(string? fc)
{
if (fc == null || !_friendCodes.TryGetValue(fc, out var entry))
{
return null;
}
if (!entry.IsExpired)
{
return entry;
}
_friendCodes.TryRemove(fc, out _);
SaveFireAndForget();
return null;
}
public BanEntry BanIp(IPAddress ip, string reason, DateTime? expiresAt)
{
var key = Normalize(ip);
var entry = new BanEntry { Reason = reason, BannedAt = DateTime.UtcNow, BannedUntil = expiresAt };
_ips[key] = entry;
SaveFireAndForget();
return entry;
}
public BanEntry BanFriendCode(string fc, string reason, DateTime? expiresAt)
{
var entry = new BanEntry { Reason = reason, BannedAt = DateTime.UtcNow, BannedUntil = expiresAt };
_friendCodes[fc] = entry;
SaveFireAndForget();
return entry;
}
public bool UnbanIp(IPAddress ip)
{
var key = Normalize(ip);
var removed = _ips.TryRemove(key, out _);
if (removed)
{
SaveFireAndForget();
}
return removed;
}
public bool UnbanFriendCode(string fc)
{
var removed = _friendCodes.TryRemove(fc, out _);
if (removed)
{
SaveFireAndForget();
}
return removed;
}
public (int IpCount, int FriendCodeCount) Stats()
{
CleanExpired();
return (_ips.Count, _friendCodes.Count);
}
public List<BanEntry> AllIpBans()
{
CleanExpired();
return _ips.Values.ToList();
}
public List<BanEntry> AllFriendCodeBans()
{
CleanExpired();
return _friendCodes.Values.ToList();
}
private void CleanExpired()
{
foreach (var kv in _ips)
{
if (kv.Value.IsExpired)
{
_ips.TryRemove(kv.Key, out _);
}
}
foreach (var kv in _friendCodes)
{
if (kv.Value.IsExpired)
{
_friendCodes.TryRemove(kv.Key, out _);
}
}
}
private static string Normalize(IPAddress ip)
=> ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4().ToString() : ip.ToString();
public static DateTime? ParseDuration(string? duration)
{
if (string.IsNullOrWhiteSpace(duration))
{
return null;
}
var match = Regex.Match(duration.Trim(), @"^(\d+)([smhd])$", RegexOptions.IgnoreCase);
if (!match.Success)
{
return null;
}
var value = int.Parse(match.Groups[1].Value);
var unit = match.Groups[2].Value.ToLowerInvariant()[0];
var span = unit switch
{
's' => TimeSpan.FromSeconds(value),
'm' => TimeSpan.FromMinutes(value),
'h' => TimeSpan.FromHours(value),
'd' => TimeSpan.FromDays(value),
_ => TimeSpan.Zero,
};
return DateTime.UtcNow.Add(span);
}
protected override BanData GetSnapshot()
{
CleanExpired();
return new BanData
{
Ips = _ips.ToDictionary(kv => kv.Key, kv => kv.Value),
FriendCodes = _friendCodes.ToDictionary(kv => kv.Key, kv => kv.Value),
};
}
protected override void ApplySnapshot(BanData data)
{
_ips = new ConcurrentDictionary<string, BanEntry>(data.Ips ?? new Dictionary<string, BanEntry>());
_friendCodes = new ConcurrentDictionary<string, BanEntry>(data.FriendCodes ?? new Dictionary<string, BanEntry>());
}
public sealed class BanData
{
public Dictionary<string, BanEntry> Ips { get; set; } = new();
public Dictionary<string, BanEntry> FriendCodes { get; set; } = new();
}
public sealed class BanEntry
{
public string Reason { get; set; } = string.Empty;
public DateTime BannedAt { get; set; }
public DateTime? BannedUntil { get; set; }
[JsonIgnore]
public bool IsExpired => BannedUntil.HasValue && DateTime.UtcNow > BannedUntil.Value;
}
}
}
@@ -7,7 +7,7 @@ using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace Empostor.Server.Utils
namespace Empostor.Server.Service.Shared
{
public class IpGeolocationService
{
@@ -48,84 +48,48 @@ namespace Empostor.Server.Utils
{
var normalizedIp = ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4() : ip;
var url = string.Format(ApiUrl, normalizedIp);
using var client = _httpClientFactory.CreateClient("ipgeo");
using var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(3));
var json = await client.GetStringAsync(url, cts.Token);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
if (root.TryGetProperty("status", out var status) && status.GetString() == "success")
var response = await client.GetStringAsync(url);
using var doc = JsonDocument.Parse(response);
var status = doc.RootElement.GetProperty("status").GetString();
if (status != "success")
{
var country = root.TryGetProperty("country", out var c) ? c.GetString() ?? string.Empty : string.Empty;
var region = root.TryGetProperty("regionName", out var r) ? r.GetString() ?? string.Empty : string.Empty;
var city = root.TryGetProperty("city", out var ct) ? ct.GetString() ?? string.Empty : string.Empty;
if (string.IsNullOrEmpty(country) && string.IsNullOrEmpty(city))
{
return string.Empty;
}
var parts = country != city
? (region != city ? $"{country} {region} {city}" : $"{country} {city}")
: country;
return parts.Trim();
return string.Empty;
}
return string.Empty;
var country = doc.RootElement.TryGetProperty("country", out var c) ? c.GetString() : string.Empty;
var region = doc.RootElement.TryGetProperty("regionName", out var r) ? r.GetString() : string.Empty;
var city = doc.RootElement.TryGetProperty("city", out var ci) ? ci.GetString() : string.Empty;
return $"{country} {region} {city}".Trim();
}
catch (Exception ex)
{
_logger.LogDebug(ex, "IP geolocation lookup failed for {Ip}", ip);
_logger.LogDebug(ex, "IP geolocation query failed for {Ip}", ip);
return string.Empty;
}
}
private static bool IsPrivateIp(IPAddress ip)
{
if (ip.IsIPv4MappedToIPv6)
if (IPAddress.IsLoopback(ip))
{
ip = ip.MapToIPv4();
return true;
}
if (ip.AddressFamily != AddressFamily.InterNetwork)
{
return true; // Don't query non-IPv4 for now
return true;
}
var bytes = ip.GetAddressBytes();
if (bytes.Length != 4)
return bytes[0] switch
{
return true;
}
// 127.0.0.0/8
if (bytes[0] == 127)
{
return true;
}
// 10.0.0.0/8
if (bytes[0] == 10)
{
return true;
}
// 172.16.0.0/12
if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31)
{
return true;
}
// 192.168.0.0/16
if (bytes[0] == 192 && bytes[1] == 168)
{
return true;
}
return false;
10 => true,
172 => bytes[1] >= 16 && bytes[1] <= 31,
192 => bytes[1] == 168,
_ => false,
};
}
}
}
@@ -4,7 +4,7 @@ using Empostor.Api.Events;
using Empostor.Api.Events.Client;
using Microsoft.Extensions.Logging;
namespace Empostor.Server.Service.Admin.Reactor
namespace Empostor.Server.Service.Shared
{
// Based on Reactor.Empostor protocol:
// https://github.com/NuclearPowered/Reactor.Empostor
@@ -1,7 +1,7 @@
using System.Runtime.CompilerServices;
using Empostor.Api.Net;
namespace Empostor.Server.Service.Admin.Reactor
namespace Empostor.Server.Service.Shared
{
public static class ReactorModExtensions
@@ -4,7 +4,7 @@ using System.Linq;
using Empostor.Api.Service;
using Microsoft.Extensions.Logging;
namespace Empostor.Server.Service.Admin.Report
namespace Empostor.Server.Service.Shared
{
public sealed class ReportStore : JsonDataStore<List<ReportStore.ReportEntry>>
{
+45
View File
@@ -47,6 +47,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Empostor.Plugins.ChatFilter
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Empostor.Plugins.PlayerStats", "Empostor.Plugins.PlayerStats\Empostor.Plugins.PlayerStats.csproj", "{2C33B35B-3A9F-4EBA-A7FA-5264E083BBB2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Empostor.Plugins.PlayerLog", "Empostor.Plugins.PlayerLog\Empostor.Plugins.PlayerLog.csproj", "{135D8F7F-C330-444E-94D4-D2D3944F688A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Empostor.Plugins.Monitor", "Empostor.Plugins.Monitor\Empostor.Plugins.Monitor.csproj", "{B50665B5-AEDB-4F56-B54A-542863EDB009}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Empostor.Plugins.Privacy", "Empostor.Plugins.Privacy\Empostor.Plugins.Privacy.csproj", "{959879FE-2FE0-4FD8-A706-0FA1B3138F45}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -297,6 +303,42 @@ Global
{2C33B35B-3A9F-4EBA-A7FA-5264E083BBB2}.Release|x64.Build.0 = Release|Any CPU
{2C33B35B-3A9F-4EBA-A7FA-5264E083BBB2}.Release|x86.ActiveCfg = Release|Any CPU
{2C33B35B-3A9F-4EBA-A7FA-5264E083BBB2}.Release|x86.Build.0 = Release|Any CPU
{135D8F7F-C330-444E-94D4-D2D3944F688A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{135D8F7F-C330-444E-94D4-D2D3944F688A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{135D8F7F-C330-444E-94D4-D2D3944F688A}.Debug|x64.ActiveCfg = Debug|Any CPU
{135D8F7F-C330-444E-94D4-D2D3944F688A}.Debug|x64.Build.0 = Debug|Any CPU
{135D8F7F-C330-444E-94D4-D2D3944F688A}.Debug|x86.ActiveCfg = Debug|Any CPU
{135D8F7F-C330-444E-94D4-D2D3944F688A}.Debug|x86.Build.0 = Debug|Any CPU
{135D8F7F-C330-444E-94D4-D2D3944F688A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{135D8F7F-C330-444E-94D4-D2D3944F688A}.Release|Any CPU.Build.0 = Release|Any CPU
{135D8F7F-C330-444E-94D4-D2D3944F688A}.Release|x64.ActiveCfg = Release|Any CPU
{135D8F7F-C330-444E-94D4-D2D3944F688A}.Release|x64.Build.0 = Release|Any CPU
{135D8F7F-C330-444E-94D4-D2D3944F688A}.Release|x86.ActiveCfg = Release|Any CPU
{135D8F7F-C330-444E-94D4-D2D3944F688A}.Release|x86.Build.0 = Release|Any CPU
{B50665B5-AEDB-4F56-B54A-542863EDB009}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B50665B5-AEDB-4F56-B54A-542863EDB009}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B50665B5-AEDB-4F56-B54A-542863EDB009}.Debug|x64.ActiveCfg = Debug|Any CPU
{B50665B5-AEDB-4F56-B54A-542863EDB009}.Debug|x64.Build.0 = Debug|Any CPU
{B50665B5-AEDB-4F56-B54A-542863EDB009}.Debug|x86.ActiveCfg = Debug|Any CPU
{B50665B5-AEDB-4F56-B54A-542863EDB009}.Debug|x86.Build.0 = Debug|Any CPU
{B50665B5-AEDB-4F56-B54A-542863EDB009}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B50665B5-AEDB-4F56-B54A-542863EDB009}.Release|Any CPU.Build.0 = Release|Any CPU
{B50665B5-AEDB-4F56-B54A-542863EDB009}.Release|x64.ActiveCfg = Release|Any CPU
{B50665B5-AEDB-4F56-B54A-542863EDB009}.Release|x64.Build.0 = Release|Any CPU
{B50665B5-AEDB-4F56-B54A-542863EDB009}.Release|x86.ActiveCfg = Release|Any CPU
{B50665B5-AEDB-4F56-B54A-542863EDB009}.Release|x86.Build.0 = Release|Any CPU
{959879FE-2FE0-4FD8-A706-0FA1B3138F45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{959879FE-2FE0-4FD8-A706-0FA1B3138F45}.Debug|Any CPU.Build.0 = Debug|Any CPU
{959879FE-2FE0-4FD8-A706-0FA1B3138F45}.Debug|x64.ActiveCfg = Debug|Any CPU
{959879FE-2FE0-4FD8-A706-0FA1B3138F45}.Debug|x64.Build.0 = Debug|Any CPU
{959879FE-2FE0-4FD8-A706-0FA1B3138F45}.Debug|x86.ActiveCfg = Debug|Any CPU
{959879FE-2FE0-4FD8-A706-0FA1B3138F45}.Debug|x86.Build.0 = Debug|Any CPU
{959879FE-2FE0-4FD8-A706-0FA1B3138F45}.Release|Any CPU.ActiveCfg = Release|Any CPU
{959879FE-2FE0-4FD8-A706-0FA1B3138F45}.Release|Any CPU.Build.0 = Release|Any CPU
{959879FE-2FE0-4FD8-A706-0FA1B3138F45}.Release|x64.ActiveCfg = Release|Any CPU
{959879FE-2FE0-4FD8-A706-0FA1B3138F45}.Release|x64.Build.0 = Release|Any CPU
{959879FE-2FE0-4FD8-A706-0FA1B3138F45}.Release|x86.ActiveCfg = Release|Any CPU
{959879FE-2FE0-4FD8-A706-0FA1B3138F45}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -319,6 +361,9 @@ Global
{70582074-88B9-419C-85ED-46536AE2D6AA} = {E0009DCB-32A6-45C4-A06C-7F62D4091A29}
{AF4C4FF8-47C8-4E89-9DD4-3BD1781E18CD} = {E0009DCB-32A6-45C4-A06C-7F62D4091A29}
{2C33B35B-3A9F-4EBA-A7FA-5264E083BBB2} = {E0009DCB-32A6-45C4-A06C-7F62D4091A29}
{135D8F7F-C330-444E-94D4-D2D3944F688A} = {E0009DCB-32A6-45C4-A06C-7F62D4091A29}
{B50665B5-AEDB-4F56-B54A-542863EDB009} = {E0009DCB-32A6-45C4-A06C-7F62D4091A29}
{959879FE-2FE0-4FD8-A706-0FA1B3138F45} = {E0009DCB-32A6-45C4-A06C-7F62D4091A29}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DAF5625C-7E46-4DCC-AF2C-11D20D965454}