mirror of
https://github.com/Empostor/Empostor.git
synced 2026-10-20 01:46:07 +00:00
Enhanced Titles & Welcome Plugins with Anti-Cheat
This commit is contained in:
1 parent
0a5e3b3173
commit
e44efc79c2
25 files changed
+426
-59
No files matched your search
Binary file not shown.
Binary file not shown.
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
"EnableOwnershipChecks": true,
|
||||
"EnableRoleChecks": true,
|
||||
"EnableTargetChecks": true,
|
||||
"ForbidProtocolExtensions": true
|
||||
"ForbidProtocolExtensions": true,
|
||||
"EnablePacketSizeChecks": true,
|
||||
"PacketSizeLimit": 1203
|
||||
},
|
||||
"Timeout": {
|
||||
"SpawnTimeout": 2500,
|
||||
|
||||
@@ -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, )",
|
||||
|
||||
Reference in new issue
Block a user