Enhanced Titles & Welcome Plugins with Anti-Cheat

This commit is contained in:
linmeideli committed 2026-08-01 00:03:03 +08:00
1 parent 0a5e3b3173
commit e44efc79c2
25 files changed
+426 -59

No files matched your search

Binary file not shown.
Binary file not shown.
+10
View File
@@ -45,6 +45,11 @@
"version": "1.1.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/master/marketplace/Empostor.Plugins.Welcome1.1.0.dll"
},
{
"version": "1.2.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/master/marketplace/Empostor.Plugins.Welcome1.2.0.dll"
}
]
},
@@ -84,6 +89,11 @@
"version": "1.0.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/master/marketplace/Empostor.Plugins.Titles.dll"
},
{
"version": "1.1.0",
"empostor_version": "2.0.0",
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/master/marketplace/Empostor.Plugins.Titles1.1.0.dll"
}
]
},
+3
View File
@@ -29,6 +29,9 @@ public enum CheatCategory
/// <summary>A packet was sent on an invalid network object, like a PlayerControl without PlayerInfo.</summary>
InvalidObject,
/// <summary>A packet was sent that exceeded the maximum allowed RPC size.</summary>
PacketSize,
/// <summary>Legacy category for unsorted anticheat checks.</summary>
Other,
}
@@ -27,5 +27,9 @@ namespace Empostor.Api.Config
public bool EnableTargetChecks { get; set; } = true;
public bool ForbidProtocolExtensions { get; set; } = true;
public bool EnablePacketSizeChecks { get; set; } = true;
public int PacketSizeLimit { get; set; } = 1203;
}
}
@@ -3,10 +3,14 @@ using Empostor.Api.Net;
namespace Empostor.Api.Events.Client
{
/// <summary>
/// Called just after a <see cref="IClient"/> is created and connected.
/// Called after a <see cref="IClient"/> has been fully registered
/// and all auth data (including friend code) has been resolved.
/// </summary>
public interface IClientConnectedEvent : IClientEvent
{
/// <summary>
/// Gets the registered <see cref="IClient"/>.
/// </summary>
IClient Client { get; }
}
}
+2 -2
View File
@@ -20,12 +20,12 @@ namespace Empostor.Api.Net
int Id { get; set; }
/// <summary>
/// Gets the name that was provided by the player in the client.
/// Gets or sets the name that was provided by the player in the client.
/// </summary>
/// <remarks>
/// The name is provided by the player and should not be used to store persisted data.
/// </remarks>
string Name { get; }
string Name { get; set; }
/// <summary>
/// Gets the language.
-6
View File
@@ -34,12 +34,6 @@
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.28, )",
"resolved": "8.0.28",
"contentHash": "XMqgVjlLxLqWmEh3c49haXLQwsMNtvo6YscUaqfvEGfg1iA8hnYgkUVq3i9Zu9gKeNKMWiiZKVwZExc/qyEAsQ=="
},
"StyleCop.Analyzers": {
"type": "Direct",
"requested": "[1.2.0-beta.556, )",
@@ -1,9 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Empostor.Api.Events;
using Empostor.Api.Events.Game.Player;
using Empostor.Api.Events.Client;
using Microsoft.Extensions.Logging;
namespace Empostor.Plugins.Titles.Service;
@@ -11,49 +10,66 @@ namespace Empostor.Plugins.Titles.Service;
public sealed class FriendCodeTitleListener : IEventListener
{
private readonly ILogger<FriendCodeTitleListener> _logger;
private readonly Dictionary<string, string> _map;
private readonly TitlesConfig _config;
private Dictionary<string, string> _map;
public FriendCodeTitleListener(
ILogger<FriendCodeTitleListener> logger,
TitlesConfig config)
{
_logger = logger;
_map = config.Titles
.Where(t => !string.IsNullOrWhiteSpace(t.FriendCode) && !string.IsNullOrWhiteSpace(t.Title))
.ToDictionary(t => t.FriendCode, t => t.Title, StringComparer.OrdinalIgnoreCase);
_config = config;
_map = BuildMap();
_logger.LogInformation("[FriendCodeTitles] Loaded {Count} title mapping(s).", _map.Count);
}
[EventListener]
public void OnPlayerReady(IPlayerReadyEvent e)
/// <summary>
/// Reloads the title mappings from config. Called by the API middleware
/// after a new title is added.
/// </summary>
public void Reload()
{
var fc = e.ClientPlayer.Client.FriendCode;
_map = BuildMap();
_logger.LogInformation("[FriendCodeTitles] Reloaded {Count} title mapping(s).", _map.Count);
}
[EventListener]
public void OnClientConnected(IClientConnectedEvent e)
{
var fc = e.Client.FriendCode;
if (string.IsNullOrEmpty(fc)) return;
if (!_map.TryGetValue(fc, out var title)) return;
var player = e.ClientPlayer;
var ctrl = e.PlayerControl;
Task.Run(async () =>
try
{
try
var displayName = TitleStore.BuildDisplayName(title, e.Client.Name);
e.Client.Name = displayName;
_logger.LogInformation("[FriendCodeTitles] Applied [{Title}] to {Name} ({FC})",
title, e.Client.Name, fc);
// Remove applied title so it is only used once
_map.Remove(fc);
// Also remove from config so it persists across reloads
var entry = _config.Titles.FirstOrDefault(t =>
string.Equals(t.FriendCode, fc, StringComparison.OrdinalIgnoreCase));
if (entry != null)
{
await Task.Delay(TimeSpan.FromMilliseconds(600));
if (player.Client.Connection == null || !player.Client.Connection.IsConnected)
return;
var displayName = TitleStore.BuildDisplayName(title, player.Client.Name);
await ctrl.SetNameAsync(displayName);
_logger.LogInformation("[FriendCodeTitles] Applied [{Title}] to {Name} ({FC})",
title, player.Client.Name, fc);
_config.Titles.Remove(entry);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[FriendCodeTitles] Failed to apply title for {FC}", fc);
}
});
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[FriendCodeTitles] Failed to apply title for {FC}", fc);
}
}
private Dictionary<string, string> BuildMap()
{
return _config.Titles
.Where(t => !string.IsNullOrWhiteSpace(t.FriendCode) && !string.IsNullOrWhiteSpace(t.Title))
.ToDictionary(t => t.FriendCode, t => t.Title, StringComparer.OrdinalIgnoreCase);
}
}
@@ -0,0 +1,108 @@
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Empostor.Plugins.Titles.Service;
public sealed class TitleApiMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<TitleApiMiddleware> _logger;
private readonly TitlesConfig _config;
private readonly string _configPath;
private readonly FriendCodeTitleListener? _friendCodeListener;
public TitleApiMiddleware(
RequestDelegate next,
ILogger<TitleApiMiddleware> logger,
TitlesConfig config,
string configPath,
FriendCodeTitleListener? friendCodeListener = null)
{
_next = next;
_logger = logger;
_config = config;
_configPath = configPath;
_friendCodeListener = friendCodeListener;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Path == "/api/title/add"
&& context.Request.Method == "POST")
{
await HandleAddTitleAsync(context);
return;
}
await _next(context);
}
private async Task HandleAddTitleAsync(HttpContext context)
{
try
{
using var reader = new StreamReader(context.Request.Body);
var body = await reader.ReadToEndAsync();
var request = JsonSerializer.Deserialize<TitleAddRequest>(body);
if (request == null
|| string.IsNullOrWhiteSpace(request.FriendCode)
|| string.IsNullOrWhiteSpace(request.Title))
{
context.Response.StatusCode = 400;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(
"{\"ok\":false,\"error\":\"friendCode and title are required.\"}");
return;
}
// Add to config
_config.Titles.Add(new FriendCodeTitle
{
FriendCode = request.FriendCode.Trim(),
Title = request.Title.Trim()
});
// Persist to file
var json = JsonSerializer.Serialize(_config, new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
});
await File.WriteAllTextAsync(_configPath, json);
// Reload mappings in the listener
_friendCodeListener?.Reload();
var addedBy = request.AddedBy ?? "unknown";
_logger.LogInformation(
"[TitleApi] Added title [{Title}] for friend code {FC} (added by {AddedBy})",
request.Title, request.FriendCode, addedBy);
context.Response.StatusCode = 200;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync("{\"ok\":true}");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[TitleApi] Failed to add title");
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(
"{\"ok\":false,\"error\":\"Internal server error.\"}");
}
}
private sealed record TitleAddRequest(
[property: JsonPropertyName("friendCode")]
string? FriendCode,
[property: JsonPropertyName("title")]
string? Title,
[property: JsonPropertyName("addedBy")]
string? AddedBy);
}
@@ -20,19 +20,21 @@ public sealed class TitleEventListener : IEventListener
[EventListener]
public void OnPlayerSpawned(IPlayerSpawnedEvent e)
{
// Titles are now applied natively via IClientConnectedEvent in FriendCodeTitleListener.
// TitleStore-based titles (set via API) are still available via the store.
var clientId = e.ClientPlayer.Client.Id;
var title = _store.Get(clientId);
if (title == null) return;
// For API-applied titles on already-connected players, we still use SetNameAsync
// since the client.Name was already set before the API title was added.
var player = e.ClientPlayer;
var playerCtrl = e.PlayerControl;
Task.Run(async () =>
_ = Task.Run(async () =>
{
try
{
await Task.Delay(TimeSpan.FromMilliseconds(600));
if (player.Client.Connection == null || !player.Client.Connection.IsConnected)
return;
+1 -1
View File
@@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging;
namespace Empostor.Plugins.Titles;
[EmpostorPlugin("cn.Empostor.titles", "Title System", "Empostor", "1.0.0")]
[EmpostorPlugin("cn.Empostor.titles", "Title System", "Empostor", "1.1.0")]
public sealed class TitlesPlugin : PluginBase, IPluginLanguageProvider
{
private readonly ILogger<TitlesPlugin> _logger;
@@ -1,12 +1,13 @@
using Empostor.Api.Events;
using Empostor.Api.Plugins;
using Empostor.Plugins.Titles.Service;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Empostor.Plugins.Titles;
public sealed class TitlesPluginStartup : IPluginStartup
public sealed class TitlesPluginStartup : IPluginHttpStartup
{
private string _configPath = "[Title System]Config.json";
@@ -17,7 +18,13 @@ public sealed class TitlesPluginStartup : IPluginStartup
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(_ => PluginConfigLoader.Load<TitlesConfig>(_configPath));
services.AddSingleton<TitleStore>();
services.AddSingleton<IEventListener, TitleEventListener>();
services.AddSingleton<IEventListener, FriendCodeTitleListener>();
}
public void ConfigureWebApplication(IApplicationBuilder builder)
{
builder.UseMiddleware<TitleApiMiddleware>();
}
}
@@ -132,6 +132,7 @@
"NextFast.Hazel.Abstractions": {
"type": "Project"
}
}
},
"net8.0/win-x64": {}
}
}
@@ -6,5 +6,6 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Empostor.Api\Empostor.Api.csproj" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
</ItemGroup>
</Project>
@@ -1,5 +1,8 @@
using System;
using System.IO;
using System.Net.Http;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Empostor.Api.Events;
using Empostor.Api.Service;
@@ -14,11 +17,20 @@ public sealed class WelcomeEventListener : IEventListener
private const string TextDir = "Message";
private const string FallbackFile = "EnglishHelloWord.txt";
private readonly ILogger<WelcomeEventListener> _logger;
private static readonly Regex CaveRegex = new(
@"\<cave\>(.+?)\</cave\>",
RegexOptions.Compiled | RegexOptions.IgnoreCase,
TimeSpan.FromMilliseconds(100));
public WelcomeEventListener(ILogger<WelcomeEventListener> logger)
private readonly ILogger<WelcomeEventListener> _logger;
private readonly IHttpClientFactory _httpClientFactory;
public WelcomeEventListener(
ILogger<WelcomeEventListener> logger,
IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
}
[EventListener]
@@ -50,6 +62,10 @@ public sealed class WelcomeEventListener : IEventListener
}
var template = File.ReadAllText(filePath);
// Process <cave>URL</cave> tags before final formatting
template = await ProcessCaveTagsAsync(template);
var message = FormatMessage(template, player);
await playerCtrl.SendChatToPlayerAsync(message, playerCtrl);
@@ -61,6 +77,116 @@ public sealed class WelcomeEventListener : IEventListener
});
}
/// <summary>
/// Finds all <cave>URL</cave> tags in the template, fetches content from each URL,
/// and replaces the tags with the fetched text.
/// </summary>
private async Task<string> ProcessCaveTagsAsync(string template)
{
var matches = CaveRegex.Matches(template);
if (matches.Count == 0) return template;
foreach (Match match in matches)
{
var url = match.Groups[1].Value.Trim();
var replacement = await FetchCaveContentAsync(url);
template = template.Replace(match.Value, replacement);
}
return template;
}
/// <summary>
/// Fetches content from a URL. Supports:
/// - Plain text responses (returned as-is)
/// - JSON responses with a "hitokoto" field (Hitokoto API format)
/// - JSON responses with a "content" field
/// Returns an empty string on failure.
/// </summary>
private async Task<string> FetchCaveContentAsync(string url)
{
try
{
using var client = _httpClientFactory.CreateClient("cave");
client.Timeout = TimeSpan.FromSeconds(5);
var response = await client.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("[Welcome] <cave> fetch failed for {Url}: HTTP {Status}",
url, (int)response.StatusCode);
return string.Empty;
}
var body = await response.Content.ReadAsStringAsync();
// Try to detect content type
var contentType = response.Content.Headers.ContentType?.MediaType;
if (contentType != null && contentType.Contains("json", StringComparison.OrdinalIgnoreCase))
{
return TryParseJsonContent(body);
}
// If content-type is not JSON, try to detect JSON anyway
if (body.TrimStart().StartsWith("{"))
{
return TryParseJsonContent(body);
}
// Plain text — return directly
return body.Trim();
}
catch (TaskCanceledException)
{
_logger.LogWarning("[Welcome] <cave> fetch timed out for {Url}", url);
return string.Empty;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[Welcome] <cave> fetch error for {Url}", url);
return string.Empty;
}
}
/// <summary>
/// Tries to parse JSON content looking for known fields.
/// Falls back to returning the raw trimmed string.
/// </summary>
private static string TryParseJsonContent(string body)
{
try
{
using var doc = JsonDocument.Parse(body);
var root = doc.RootElement;
// Hitokoto API: {"hitokoto": "...", "from": "..."}
if (root.TryGetProperty("hitokoto", out var hitokoto))
{
var text = hitokoto.GetString() ?? string.Empty;
if (root.TryGetProperty("from", out var from) && !string.IsNullOrWhiteSpace(from.GetString()))
{
text += $" —— {from.GetString()}";
}
return text;
}
// Generic: {"content": "..."}
if (root.TryGetProperty("content", out var content))
{
return content.GetString() ?? string.Empty;
}
// Unknown JSON — return raw
return body.Trim();
}
catch
{
return body.Trim();
}
}
private static string FormatMessage(string template, Api.Net.IClientPlayer player)
{
var lastConnect = player.Client.ProductUserId != null
@@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging;
namespace Empostor.Plugins.Welcome;
[EmpostorPlugin("cn.hayashiume.welcome", "Welcome Messages", "HayashiUme", "1.1.0")]
[EmpostorPlugin("cn.hayashiume.welcome", "Welcome Messages", "HayashiUme", "1.2.0")]
public sealed class WelcomePlugin : PluginBase
{
private const string TextDir = "Message";
@@ -12,6 +12,7 @@ public sealed class WelcomePluginStartup : IPluginStartup
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
services.AddSingleton<IEventListener, WelcomeEventListener>();
}
}
@@ -2,6 +2,20 @@
"version": 1,
"dependencies": {
"net8.0": {
"Microsoft.Extensions.Http": {
"type": "Direct",
"requested": "[8.0.0, )",
"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"
}
},
"BouncyCastle.Cryptography": {
"type": "Transitive",
"resolved": "2.6.1",
@@ -24,6 +38,15 @@
"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",
@@ -32,11 +55,37 @@
"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.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.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",
@@ -67,6 +116,16 @@
"Microsoft.Extensions.Logging.Abstractions": "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",
@@ -89,6 +148,18 @@
"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",
@@ -132,6 +203,7 @@
"NextFast.Hazel.Abstractions": {
"type": "Project"
}
}
},
"net8.0/win-x64": {}
}
}
+1
View File
@@ -75,6 +75,7 @@ namespace Empostor.Server.Net
CheatCategory.ProtocolExtension => _antiCheatConfig.ForbidProtocolExtensions,
CheatCategory.GameFlow => _antiCheatConfig.EnableGameFlowChecks,
CheatCategory.InvalidObject => _antiCheatConfig.EnableInvalidObjectChecks,
CheatCategory.PacketSize => _antiCheatConfig.EnablePacketSizeChecks,
CheatCategory.MustBeHost => _antiCheatConfig.EnableMustBeHostChecks,
CheatCategory.ColorLimits => _antiCheatConfig.EnableColorLimitChecks,
CheatCategory.NameLimits => _antiCheatConfig.EnableNameLimitChecks,
+1 -1
View File
@@ -29,7 +29,7 @@ namespace Empostor.Server.Net
/// </summary>
public int DeltaPort { get; set; }
public string Name { get; }
public string Name { get; set; }
public Language Language { get; }
@@ -1,18 +1,23 @@
using System.Net;
using System.Threading.Tasks;
using Empostor.Api;
using Empostor.Api.Config;
using Empostor.Api.Net;
using Next.Hazel;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Empostor.Server.Net.Hazel
{
internal class HazelConnection : IHazelConnection
{
private readonly ILogger<HazelConnection> _logger;
private readonly AntiCheatConfig _antiCheatConfig;
public HazelConnection(Connection innerConnection, ILogger<HazelConnection> logger)
public HazelConnection(Connection innerConnection, ILogger<HazelConnection> logger, IOptions<AntiCheatConfig> antiCheatOptions)
{
_logger = logger;
_antiCheatConfig = antiCheatOptions.Value;
InnerConnection = innerConnection;
innerConnection.DataReceived = ConnectionOnDataReceived;
innerConnection.Disconnected = ConnectionOnDisconnected;
@@ -58,6 +63,19 @@ namespace Empostor.Server.Net.Hazel
return;
}
// Check raw message size against the configured limit.
// Innersloth requires full packet ≤ 1200 bytes (1168 bytes payload after 32 bytes IP+UDP headers).
if (e.Message.Length > _antiCheatConfig.PacketSizeLimit)
{
if (await Client.ReportCheatAsync(
new CheatContext("RootMessage"),
CheatCategory.PacketSize,
$"Received a message that is too large, length: {e.Message.Length}"))
{
return;
}
}
while (true)
{
if (e.Message.Position >= e.Message.Length)
+5 -2
View File
@@ -29,6 +29,7 @@ namespace Empostor.Server.Net
private readonly IFirewallService _firewall;
private readonly ServerConfig _serverConfig;
private readonly AuthCacheService _authCache;
private readonly IOptions<AntiCheatConfig> _antiCheatOptions;
private UdpConnectionListener? _mainListener;
private readonly ConcurrentDictionary<int, UdpConnectionListener> _deltaListeners = new();
@@ -44,7 +45,8 @@ namespace Empostor.Server.Net
PortPoolService portPool,
IFirewallService firewall,
IOptions<ServerConfig> serverConfig,
AuthCacheService authCache)
AuthCacheService authCache,
IOptions<AntiCheatConfig> antiCheatOptions)
{
_eventManager = eventManager;
_clientManager = clientManager;
@@ -55,6 +57,7 @@ namespace Empostor.Server.Net
_firewall = firewall;
_serverConfig = serverConfig.Value;
_authCache = authCache;
_antiCheatOptions = antiCheatOptions;
// Subscribe to port return events from the pool
_portPool.OnPortReturned += OnPortReturned;
@@ -182,7 +185,7 @@ namespace Empostor.Server.Net
out var chatMode,
out var platformSpecificData);
var connection = new HazelConnection(e.Connection, _connectionLogger);
var connection = new HazelConnection(e.Connection, _connectionLogger, _antiCheatOptions);
await _eventManager.CallAsync(new ClientConnectionEvent(connection, e.HandshakeData));
await _clientManager.RegisterConnectionAsync(
connection, name, clientVersion, language, chatMode, platformSpecificData,
+3 -1
View File
@@ -25,7 +25,9 @@
"EnableOwnershipChecks": true,
"EnableRoleChecks": true,
"EnableTargetChecks": true,
"ForbidProtocolExtensions": true
"ForbidProtocolExtensions": true,
"EnablePacketSizeChecks": true,
"PacketSizeLimit": 1203
},
"Timeout": {
"SpawnTimeout": 2500,
-6
View File
@@ -58,12 +58,6 @@
"resolved": "8.0.1",
"contentHash": "X21b8D6z58EG34OMLH955750mFaEaGs5xLGe9Vyi3059Cgb3sk+p2k/JlQXaieEBRrk+SVlFzDei3Q6PdVzJOA=="
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.28, )",
"resolved": "8.0.28",
"contentHash": "XMqgVjlLxLqWmEh3c49haXLQwsMNtvo6YscUaqfvEGfg1iA8hnYgkUVq3i9Zu9gKeNKMWiiZKVwZExc/qyEAsQ=="
},
"Serilog.Extensions.Hosting": {
"type": "Direct",
"requested": "[8.0.0, )",