Admin Client Details Add

This commit is contained in:
linmeideli committed 2026-07-04 13:41:21 +08:00
1 parent 7dfcec19cf
commit 6248d3c0cc
12 files changed
+124 -54

No files matched your search

@@ -150,7 +150,6 @@
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
}
},
".NETStandard,Version=v2.0/win-x64": {}
}
}
}
+1 -8
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, )",
@@ -145,7 +139,6 @@
"NextFast.Hazel.Abstractions": {
"type": "Project"
}
},
"net8.0/win-x64": {}
}
}
}
+22 -12
View File
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Text.Json;
using Empostor.Api;
using Empostor.Api.Events.Player;
using Microsoft.Extensions.Logging;
@@ -21,25 +22,34 @@ public sealed class ChatService
public void HandleChatMessage(IPlayerChatEvent e)
{
var playerInfo = e.PlayerControl.PlayerInfo;
var playerName = playerInfo?.PlayerName ?? "?";
var ownerId = e.PlayerControl.OwnerId;
var client = e.ClientPlayer.Client;
var clientId = client.Id;
var playerName = client.Name;
var slot = e.ClientPlayer.Character?.PlayerId ?? 0;
var gameCode = Api.Innersloth.GameCodeParser.IntToGameName(e.Game.Code);
if (e.ClientPlayer.Character == e.PlayerControl)
int channel;
string channelName;
if (e.IsCancelled)
{
_logger.LogInformation(
"[{GameCode}] {PlayerName} ({PlayerId}): {Message}",
e.Game.Code, playerName, ownerId, e.Message);
channel = 0;
channelName = "Canceled";
}
else if (!e.SendToAllPlayers)
{
channel = 255;
channelName = "Command";
}
else
{
var senderName = e.ClientPlayer.Character?.PlayerInfo?.PlayerName ?? "?";
var senderId = e.ClientPlayer.Client.Id;
_logger.LogInformation(
"[{GameCode}] {SenderName} ({SenderId}) on behalf of {PlayerName} ({PlayerId}): {Message}",
e.Game.Code, senderName, senderId, playerName, ownerId, e.Message);
channel = -1;
channelName = "Public";
}
_logger.LogInformation(
"{Code} - [{ClientId}][{Name}][{Slot}] => [{Channel}]{ChannelName}: [{Message}]",
gameCode, clientId, playerName, slot, channel, channelName, e.Message);
var isHost = e.ClientPlayer.IsHost;
var maxLength = isHost ? _config.HostMaxMessageLength : _config.PlayerMaxMessageLength;
+22 -4
View File
@@ -20,6 +20,7 @@ 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.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@@ -48,6 +49,7 @@ namespace Empostor.Server.Http
private readonly PlayerStatsConfig _statsConfig;
private readonly ChatFilterStore _chatFilter;
private readonly DiscordWebhookStore _discordWebhook;
private readonly IpGeolocationService _ipGeo;
private readonly string _passwordHash;
private static readonly ConcurrentDictionary<string, (int Count, DateTime FirstAttempt)> _loginFailures = new();
@@ -62,7 +64,8 @@ namespace Empostor.Server.Http
PlayerStatsStore playerStats,
IOptions<PlayerStatsConfig> statsConfig,
ChatFilterStore chatFilter,
DiscordWebhookStore discordWebhook)
DiscordWebhookStore discordWebhook,
IpGeolocationService ipGeo)
{
_logger = logger;
_gameManager = gameManager;
@@ -75,6 +78,7 @@ namespace Empostor.Server.Http
_statsConfig = statsConfig.Value;
_chatFilter = chatFilter;
_discordWebhook = discordWebhook;
_ipGeo = ipGeo;
_passwordHash = ComputeHash(_config.Password);
if (string.IsNullOrEmpty(_config.Password)
@@ -229,14 +233,23 @@ namespace Empostor.Server.Http
}
[HttpGet("/api/admin/clients")]
public IActionResult GetClients()
public async Task<IActionResult> GetClients()
{
if (!IsAuthenticated())
{
return Unauthorized();
}
return Ok(_clientManager.Clients.Select(CSnap));
var clients = _clientManager.Clients.ToList();
var snaps = new List<object>(clients.Count);
foreach (var c in clients)
{
var ip = c.Connection?.EndPoint?.Address;
var location = await _ipGeo.GetLocationAsync(ip);
snaps.Add(CSnap(c, location));
}
return Ok(snaps);
}
[HttpGet("/api/admin/bans")]
@@ -803,9 +816,10 @@ namespace Empostor.Server.Http
}).ToList(),
};
private static object CSnap(IClient c)
private static object CSnap(IClient c, string location = "")
{
var reactor = c.GetReactorMods();
var level = c.Player?.Character?.PlayerInfo?.PlayerLevel;
return new
{
id = c.Id,
@@ -813,9 +827,13 @@ namespace Empostor.Server.Http
friendCode = c.FriendCode ?? "—",
gameVersion = c.GameVersion.ToString(),
platform = c.PlatformSpecificData?.Platform.ToString() ?? "Unknown",
platformName = c.PlatformSpecificData?.PlatformName ?? "",
language = LanguageHelper.GetDisplayName(c.Language),
level = level is null or 0 or uint.MaxValue ? (int?)null : (int)level.Value,
inGame = c.Player != null,
gameCode = c.Player != null ? GameCodeParser.IntToGameName(c.Player.Game.Code) : "—",
ip = c.Connection?.EndPoint?.Address?.ToString() ?? "—",
location = location,
reactor = reactor == null ? null : new
{
protocolVersion = reactor.ProtocolVersion,
@@ -1006,6 +1006,12 @@ namespace Empostor.Server.Net.Inner.Objects
}
else
{
if (PlayerInfo.PlayerLevel != level)
{
_logger.LogInformation("{Code} - Player {Name} ({Id}) level changed {Old} -> {New}",
_game.Code, sender.Client.Name, sender.Client.Id, PlayerInfo.PlayerLevel, level);
}
PlayerInfo.PlayerLevel = level;
}
@@ -1146,10 +1152,6 @@ namespace Empostor.Server.Net.Inner.Objects
private async ValueTask<bool> HandleSendChat(ClientPlayer sender, string message)
{
var clientId = sender.Client.Id;
var playerName = sender.Client.Name;
var slot = sender.Character?.PlayerId ?? 0;
if (message.StartsWith("#"))
{
var ctx = new CommandContext
@@ -1166,10 +1168,7 @@ namespace Empostor.Server.Net.Inner.Objects
};
var handled = await _commandService.TryHandleAsync(ctx);
if (handled)
{
_logger.LogInformation("[{ClientId}][{Name}][{Slot}][{Name}] => [255]Command:\n [{Message}]", clientId, playerName, slot, message);
return false;
}
}
var @event = new PlayerChatEvent(Game, sender, this, message);
@@ -1181,12 +1180,10 @@ namespace Empostor.Server.Net.Inner.Objects
await _eventManager.CallAsync(@event);
if (@event.IsCancelled)
{
_logger.LogInformation("[{ClientId}][{Name}][{Slot}][{Name}] => [0]Canceled:\n [{Message}]", clientId, playerName, slot, message);
return false;
}
else if (@event.SendToAllPlayers == false)
{
_logger.LogInformation("[{ClientId}][{Name}][{Slot}][{Name}] => [255]Command:\n [{Message}]", clientId, playerName, slot, message);
if (Game.Host != null)
{
await SendChatToPlayerAsync(message, Game.Host.Character);
@@ -1196,7 +1193,6 @@ namespace Empostor.Server.Net.Inner.Objects
}
else
{
_logger.LogInformation("[{ClientId}][{Name}][{Slot}][{Name}] => [-1]Public:\n [{Message}]", clientId, playerName, slot, message);
return true;
}
}
@@ -13,6 +13,7 @@ using Empostor.Api.Net.Manager;
using Empostor.Server.Events.Client;
using Empostor.Server.Net.Factories;
using Empostor.Api.Service;
using Empostor.Server.Service.Admin.Reactor;
using Empostor.Server.Service.Auth;
using Empostor.Server.Utils;
using Microsoft.Extensions.Logging;
@@ -143,6 +144,12 @@ namespace Empostor.Server.Net.Manager
UserAuthInfo? authInfo = null;
var clientIp = connection.EndPoint?.Address;
var location = await _ipGeo.GetLocationAsync(clientIp);
var lang = LanguageHelper.GetDisplayName(language);
var platformStr = platformSpecificData?.Platform.ToString() ?? "Unknown";
var reactorMods = connection.GetReactorMods();
var reactorStr = reactorMods?.Mods is { Count: > 0 } mods
? $" [Reactor: {string.Join(", ", System.Linq.Enumerable.Select(mods, m => $"{m.Id} {m.Version}"))}]"
: string.Empty;
// Primary: match by delta port (nonce)
if (deltaPort > 0)
@@ -177,8 +184,8 @@ namespace Empostor.Server.Net.Manager
}
_logger.LogInformation(
"Client [{Id}]{Name} from port {Port} {Location}is authorized as {FriendCode} {HashPuid}",
id, name, deltaPort, location, friendCode ?? "unknown", HashPuid(authInfo.ProductUserId));
"Client [{Id}]{Name} from port {Port} {Location}[{Lang}] [{Platform}] is authorized as {FriendCode} {HashPuid}{Reactor}",
id, name, deltaPort, location, lang, platformStr, friendCode ?? "unknown", HashPuid(authInfo.ProductUserId), reactorStr);
}
else
{
@@ -196,14 +203,14 @@ namespace Empostor.Server.Net.Manager
{
friendCode = authInfo.FriendCode;
_logger.LogInformation(
"Client [{Id}]{Name} from {Ip} {Location}is authorized as {FriendCode} {HashPuid}",
id, name, NormalizeIp(clientIp), location, friendCode ?? "unknown", HashPuid(authInfo.ProductUserId));
"Client [{Id}]{Name} from {Ip} {Location}[{Lang}] [{Platform}] is authorized as {FriendCode} {HashPuid}{Reactor}",
id, name, NormalizeIp(clientIp), location, lang, platformStr, friendCode ?? "unknown", HashPuid(authInfo.ProductUserId), reactorStr);
}
else
{
_logger.LogWarning(
"Client [{Id}]{Name}: no auth info found (port={Port}, ip={Ip} {Location})",
id, name, deltaPort, NormalizeIp(clientIp), location);
"Client [{Id}]{Name}: no auth info found (port={Port}, ip={Ip} {Location}) [{Lang}] [{Platform}]{Reactor}",
id, name, deltaPort, NormalizeIp(clientIp), location, lang, platformStr, reactorStr);
}
}
+12 -2
View File
@@ -6,6 +6,8 @@ using Empostor.Api.Games;
using Empostor.Api.Net;
using Next.Hazel;
using Empostor.Server.Events;
using Empostor.Server.Service.Admin.Reactor;
using Empostor.Server.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -127,9 +129,17 @@ namespace Empostor.Server.Net.State
var hashPuid = client.ProductUserId?.Length >= 9 ? client.ProductUserId[..9] : client.ProductUserId ?? "0";
var deltaPort = client.DeltaPort;
var location = await _ipGeo.GetLocationAsync(clientIp);
var lang = LanguageHelper.GetDisplayName(client.Language);
var platform = client.PlatformSpecificData?.Platform.ToString() ?? "Unknown";
var platformName = client.PlatformSpecificData?.PlatformName;
var platformStr = string.IsNullOrEmpty(platformName) ? platform : $"{platform}/{platformName}";
var reactorMods = client.GetReactorMods();
var reactorStr = reactorMods?.Mods is { Count: > 0 } mods
? $" [Reactor: {string.Join(", ", System.Linq.Enumerable.Select(mods, m => $"{m.Id} {m.Version}"))}]"
: string.Empty;
_logger.LogInformation("{Code} - Player {Name} ({HashPuid}) ({Id}) is joining from ({Ip} {Location}:{RemotePort}) with v{Version}, Authority:{Authority}, port: {DeltaPort}",
Code, sender.Client.Name, hashPuid, sender.Client.Id, ipStr, location, remotePort, version, authority, deltaPort);
_logger.LogInformation("{Code} - Player {Name} ({HashPuid}) ({Id}) [{Lang}] [{Platform}] is joining from ({Ip} {Location}:{RemotePort}) with v{Version}, Authority:{Authority}, port: {DeltaPort}{Reactor}",
Code, sender.Client.Name, hashPuid, sender.Client.Id, lang, platformStr, ipStr, location, remotePort, version, authority, deltaPort, reactorStr);
if (isNew)
{
@@ -152,6 +152,7 @@
"clients.version_label": "Version",
"clients.platform_label": "Platform",
"clients.language_label": "Language",
"clients.level_label": "Level",
"clients.ingame_label": "In Game",
"reactor.required_yes": "Yes",
@@ -15,7 +15,7 @@ namespace Empostor.Server.Service.Admin.Reactor
internal sealed class ReactorHandshakeListener : IEventListener
{
// "reactor" in ASCII (7 bytes), packed as upper 56 bits of a uint64 in little-endian
private const ulong ReactorMagic = 0x726f7463656172;
private const ulong ReactorMagic = 0x72656163746f72;
private readonly ILogger<ReactorHandshakeListener> _logger;
@@ -30,7 +30,9 @@ namespace Empostor.Server.Service.Admin.Reactor
try
{
// Skip known Among Us handshake fields to reach the Reactor suffix
// HandshakeC2S.Deserialize already consumed the AU handshake,
// so we must seek back to the beginning before skipping.
reader.Seek(0);
if (!SkipAuHandshake(reader))
{
return;
@@ -49,7 +51,7 @@ namespace Empostor.Server.Service.Admin.Reactor
// Read mod list for protocol V3+
var mods = Array.Empty<ClientMod>();
if (protocolVersion >= 3)
if (protocolVersion >= 2) // ReactorProtocolVersion.V3 = 2
{
mods = ReadModList(reader);
}
@@ -123,6 +125,18 @@ namespace Empostor.Server.Service.Admin.Reactor
return false;
}
// ProductUserId (Hazel string) — may be absent for older clients
if (CanRead(reader, 1))
{
TrySkipString(reader);
}
// CrossplayFlags (uint32, 4 bytes) — may be absent for older clients
if (CanRead(reader, 4))
{
reader.ReadUInt32();
}
return true;
}
@@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging;
namespace Empostor.Server.Utils
{
internal class IpGeolocationService
public class IpGeolocationService
{
private const string ApiUrl = "http://ip-api.com/json/{0}?lang=zh-CN&fields=status,country,regionName,city";
private static readonly TimeSpan CacheTtl = TimeSpan.FromHours(24);
@@ -0,0 +1,28 @@
using Empostor.Api.Innersloth;
namespace Empostor.Server.Utils
{
/// <summary>
/// Covers both the base Among Us SupportLangs (0-15) + LocalizeUs / ExtendedLangs (16-22).
/// </summary>
internal static class LanguageHelper
{
private static readonly string[] ExtendedLangs = { "Polish", "Turkish", "Swedish", "Lithuanian", "Czech", "華夏(文言文)", "Greek" };
public static string GetDisplayName(Language lang)
{
var val = (int)lang;
if (val >= 0 && val <= 15)
{
return $"{lang}({val})";
}
if (val >= 16 && val < 16 + ExtendedLangs.Length)
{
return $"{ExtendedLangs[val - 16]}({val})";
}
return $"Unknown({val})";
}
}
}
-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, )",