mirror of
https://github.com/Empostor/Empostor.git
synced 2026-10-20 01:46:07 +00:00
Admin Client Details Add
This commit is contained in:
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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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})";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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