Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions apps/player-management-css/src/ConnectGate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
using System.Runtime.InteropServices;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using FiveStack.Enums;
using FiveStack.Utilities;
using Microsoft.Extensions.Logging;

namespace PlayerManagement;

// A restricted server's door: the ConnectClient hook the match and practice
// plugins use, deciding against the panel's access list. A community server
// never loads either of those, so this is the only hook on it.
public partial class PlayerManagementPlugin
{
// FiveStack_ConnectClient in shared/gamedata/fivestack.gamedata.json, which
// the gamedata validator checks after every game update.
// near "CNetworkGameServerBase::ConnectClient( name=\'%s\', remote=\'%s\' )\n"
private static readonly string ConnectClientSignature = RuntimeInformation.IsOSPlatform(
OSPlatform.Linux
)
? "55 48 89 E5 41 57 49 89 D7 41 56 49 89 FE 41 55 41 54 53 89 CB 48 81 EC ? ? ? ?"
: "48 89 5C 24 18 44 89 4C 24 20 55 41 54 41 55 41 56 41 57 48 8D 6C 24 F1 48 81 EC ? ? ? ? 81 64 24 4C FF FF 0F FF";

/// <summary>
/// <c>
/// virtual CServerSideClientBase* CNetworkGameServerBase::ConnectClient(
/// const char* name,
/// ns_address* address,
/// void* netInfo,
/// C2S_CONNECT_Message* connectMsg,
/// const char* password,
/// const byte* authTicket,
/// int authTicketLength,
/// bool isLowViolence);
/// </c>
/// Built on load rather than as a static like the match and practice
/// plugins: a signature the game update broke would throw from the type
/// initializer and take bans, mutes and gags down with the door.
/// </summary>
private MemoryFunctionWithReturn<
nint,
nint,
nint,
nint,
nint,
nint,
nint,
int,
bool,
nint
>? _connectClientFunc;

private nint _passwordBuffer = nint.Zero;
private string? _bufferedPassword;

private void InstallConnectGate()
{
try
{
_connectClientFunc = new(ConnectClientSignature, Addresses.EnginePath);
_connectClientFunc.Hook(OnConnectClient, HookMode.Pre);

Logger.LogInformation("ConnectClient hook installed for access lists");
}
catch (Exception error)
{
_connectClientFunc = null;
Logger.LogError(
error,
"unable to hook ConnectClient; access lists are only enforced by kicking after join"
);
}
}

private void UninstallConnectGate()
{
try
{
_connectClientFunc?.Unhook(OnConnectClient, HookMode.Pre);
}
catch (Exception error)
{
Logger.LogError(error, "unable to remove the ConnectClient hook");
}

_connectClientFunc = null;

if (_passwordBuffer != nint.Zero)
{
Marshal.FreeCoTaskMem(_passwordBuffer);
_passwordBuffer = nint.Zero;
_bufferedPassword = null;
}
}

// Anything thrown here would unwind through the engine, so a failure
// leaves the connect exactly as the client sent it.
private HookResult OnConnectClient(DynamicHook hook)
{
try
{
ulong steamId = ServerAccessBook.TicketSteamId(
hook.GetParam<nint>(6),
hook.GetParam<int>(7)
);
eServerAccess access = _access.Decide(steamId.ToString());

if (access is eServerAccess.Unknown or eServerAccess.Open)
{
return HookResult.Continue;
}

nint password = access == eServerAccess.Allowed ? ServerPassword() : nint.Zero;

Logger.LogInformation(
"connect {steamId} '{name}': {access} by access list {version} | password swapped: {swapped}",
steamId,
hook.GetParam<string>(1) ?? "",
access,
_access.Snapshot().Version,
password != nint.Zero
);

if (access == eServerAccess.Denied)
{
hook.SetParam(6, 0);
hook.SetParam(7, 0);
}
else if (password != nint.Zero)
{
hook.SetParam(5, password);
}
}
catch (Exception error)
{
Logger.LogError(error, "access check failed; leaving the connect to the engine");
}

return HookResult.Continue;
}

// Read at connect time: the owner can change sv_password over RCON while
// the server hibernates, when nothing on a tick would notice.
private nint ServerPassword()
{
string password = ConVar.Find("sv_password")?.StringValue ?? "";

if (password.Length == 0)
{
return nint.Zero;
}

if (password != _bufferedPassword)
{
if (_passwordBuffer != nint.Zero)
{
Marshal.FreeCoTaskMem(_passwordBuffer);
}

_passwordBuffer = Marshal.StringToCoTaskMemUTF8(password);
_bufferedPassword = password;
}

return _passwordBuffer;
}
}
2 changes: 2 additions & 0 deletions apps/player-management-css/src/PlayerManagement.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Entities/PlayerManagement/**/*.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Enums/eSanctionChange.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Enums/eServerAccess.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/SanctionBook.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/ServerAccessBook.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/SanctionsClient.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/SanctionSyncLoop.cs" LinkBase="Shared" />
<Compile Include="$(MSBuildThisFileDirectory)../../../shared/dotnet/FiveStack.Utilities/PlayerManagementReport.cs" LinkBase="Shared" />
Expand Down
35 changes: 32 additions & 3 deletions apps/player-management-css/src/PlayerManagementPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace PlayerManagement;
// Community servers only: a matchmaking server gets its sanctions on the match
// payload from the match plugin, and never loads this one.
[MinimumApiVersion(80)]
public class PlayerManagementPlugin : BasePlugin, IPluginConfig<PlayerManagementConfig>
public partial class PlayerManagementPlugin : BasePlugin, IPluginConfig<PlayerManagementConfig>
{
private const string Runtime = "counterstrikesharp";

Expand All @@ -25,11 +25,12 @@ public class PlayerManagementPlugin : BasePlugin, IPluginConfig<PlayerManagement
public override string ModuleVersion => "__RELEASE_VERSION__";
public override string ModuleAuthor => "5Stack.gg";
public override string ModuleDescription =>
"Enforces 5Stack bans, mutes and gags on community servers";
"Enforces 5Stack bans, mutes, gags and access lists on community servers";

public PlayerManagementConfig Config { get; set; } = new();

private readonly SanctionBook _book = new();
private readonly ServerAccessBook _access = new();
private SanctionSyncLoop? _loop;

// What was last applied to each player present, so changes are announced
Expand Down Expand Up @@ -60,7 +61,7 @@ public override void Load(bool hotReload)
if (!settings.IsConnected())
{
Logger.LogWarning(
"player management is not configured; bans, mutes and gags are not enforced until API_DOMAIN, SERVER_ID and SERVER_API_PASSWORD are set"
"player management is not configured; bans, mutes, gags and access lists are not enforced until API_DOMAIN, SERVER_ID and SERVER_API_PASSWORD are set"
);
}

Expand All @@ -84,6 +85,7 @@ public override void Load(bool hotReload)
ulong steamId = SteamIdOf(player);

_book.Left(steamId.ToString());
_access.Left(steamId.ToString());
_applied.Remove(steamId);
_mutedByUs.Remove(steamId);
_kicked.Remove(steamId);
Expand All @@ -92,8 +94,11 @@ public override void Load(bool hotReload)
AddCommandListener("say", OnChat, HookMode.Pre);
AddCommandListener("say_team", OnChat, HookMode.Pre);

InstallConnectGate();

_loop = new SanctionSyncLoop(
_book,
_access,
new SanctionsClient(),
Config.Settings,
ModuleVersion,
Expand All @@ -108,6 +113,8 @@ public override void Unload(bool hotReload)
{
_loop?.Dispose();
_loop = null;

UninstallConnectGate();
}

[ConsoleCommand(
Expand Down Expand Up @@ -142,6 +149,7 @@ public void OnStatus(CCSPlayerController? caller, CommandInfo command)
Config.Settings(),
lastSyncAt,
lastError,
_access.Snapshot(),
Humans()
.Select(player => new PlayerManagementPlayer(
player.PlayerName,
Expand Down Expand Up @@ -238,6 +246,27 @@ private void Enforce(List<CCSPlayerController> humans)
continue;
}

// CounterStrikeSharp disconnects with a reason code only, so the
// text goes to chat on the way out.
if (_access.IsDenied(steamId.ToString()))
{
if (_kicked.Add(steamId))
{
Logger.LogInformation(
"kicking {name} ({steamId}): not on the server's access list",
player.PlayerName,
steamId
);

player.PrintToChat(
_access.KickReason(Localizer.ForPlayer(player, "access.denied"))
);
player.Disconnect(NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKED);
}

continue;
}

// Re-asserted rather than set once: the engine resets voice flags
// across a reconnect and a map change.
if (state.IsMuted && !player.VoiceFlags.HasFlag(VoiceFlags.Muted))
Expand Down
3 changes: 2 additions & 1 deletion apps/player-management-css/src/lang/ar-SA.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"sanction.ungagged": " {green}تم إلغاء منعك من الكتابة",
"sanction.reason": " {grey}السبب: {0}",
"sanction.until": " {grey}حتى {0}",
"sanction.permanent": " {grey}دائم"
"sanction.permanent": " {grey}دائم",
"access.denied": "هذا الخادم خاص: أنت لست في قائمة الوصول الخاصة به"
}
3 changes: 2 additions & 1 deletion apps/player-management-css/src/lang/da-DK.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"sanction.ungagged": " {green}Du er ikke længere gagged",
"sanction.reason": " {grey}Årsag: {0}",
"sanction.until": " {grey}Indtil {0}",
"sanction.permanent": " {grey}Permanent"
"sanction.permanent": " {grey}Permanent",
"access.denied": "Denne server er privat: du er ikke på dens adgangsliste"
}
3 changes: 2 additions & 1 deletion apps/player-management-css/src/lang/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"sanction.ungagged": " {green}Du bist nicht mehr geknebelt",
"sanction.reason": " {grey}Grund: {0}",
"sanction.until": " {grey}Bis {0}",
"sanction.permanent": " {grey}Dauerhaft"
"sanction.permanent": " {grey}Dauerhaft",
"access.denied": "Dieser Server ist privat: du stehst nicht auf seiner Zugangsliste"
}
3 changes: 2 additions & 1 deletion apps/player-management-css/src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"sanction.ungagged": " {green}You are no longer gagged",
"sanction.reason": " {grey}Reason: {0}",
"sanction.until": " {grey}Until {0}",
"sanction.permanent": " {grey}Permanent"
"sanction.permanent": " {grey}Permanent",
"access.denied": "This server is private: you are not on its access list"
}
3 changes: 2 additions & 1 deletion apps/player-management-css/src/lang/es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"sanction.ungagged": " {green}Ya no estás silenciado",
"sanction.reason": " {grey}Motivo: {0}",
"sanction.until": " {grey}Hasta {0}",
"sanction.permanent": " {grey}Permanente"
"sanction.permanent": " {grey}Permanente",
"access.denied": "Este servidor es privado: no estás en su lista de acceso"
}
3 changes: 2 additions & 1 deletion apps/player-management-css/src/lang/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"sanction.ungagged": " {green}Vous n'êtes plus bâillonné",
"sanction.reason": " {grey}Raison : {0}",
"sanction.until": " {grey}Jusqu'au {0}",
"sanction.permanent": " {grey}Permanent"
"sanction.permanent": " {grey}Permanent",
"access.denied": "Ce serveur est privé : vous n'êtes pas sur sa liste d'accès"
}
3 changes: 2 additions & 1 deletion apps/player-management-css/src/lang/it-IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"sanction.ungagged": " {green}Non sei più silenziato",
"sanction.reason": " {grey}Motivo: {0}",
"sanction.until": " {grey}Fino al {0}",
"sanction.permanent": " {grey}Permanente"
"sanction.permanent": " {grey}Permanente",
"access.denied": "Questo server è privato: non sei nella sua lista di accesso"
}
3 changes: 2 additions & 1 deletion apps/player-management-css/src/lang/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"sanction.ungagged": " {green}チャット禁止が解除されました",
"sanction.reason": " {grey}理由: {0}",
"sanction.until": " {grey}期限: {0}",
"sanction.permanent": " {grey}無期限"
"sanction.permanent": " {grey}無期限",
"access.denied": "このサーバーはプライベートです:アクセスリストに登録されていません"
}
3 changes: 2 additions & 1 deletion apps/player-management-css/src/lang/ko-KR.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"sanction.ungagged": " {green}텍스트 채팅 차단이 해제되었습니다",
"sanction.reason": " {grey}사유: {0}",
"sanction.until": " {grey}만료: {0}",
"sanction.permanent": " {grey}영구"
"sanction.permanent": " {grey}영구",
"access.denied": "비공개 서버입니다: 접근 목록에 등록되어 있지 않습니다"
}
3 changes: 2 additions & 1 deletion apps/player-management-css/src/lang/pl-PL.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"sanction.ungagged": " {green}Nie jesteś już zakneblowany",
"sanction.reason": " {grey}Powód: {0}",
"sanction.until": " {grey}Do {0}",
"sanction.permanent": " {grey}Na stałe"
"sanction.permanent": " {grey}Na stałe",
"access.denied": "Ten serwer jest prywatny: nie jesteś na jego liście dostępu"
}
3 changes: 2 additions & 1 deletion apps/player-management-css/src/lang/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"sanction.ungagged": " {green}Você não está mais silenciado",
"sanction.reason": " {grey}Motivo: {0}",
"sanction.until": " {grey}Até {0}",
"sanction.permanent": " {grey}Permanente"
"sanction.permanent": " {grey}Permanente",
"access.denied": "Este servidor é privado: você não está na lista de acesso dele"
}
3 changes: 2 additions & 1 deletion apps/player-management-css/src/lang/ru-RU.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"sanction.ungagged": " {green}Запрет чата снят",
"sanction.reason": " {grey}Причина: {0}",
"sanction.until": " {grey}До {0}",
"sanction.permanent": " {grey}Навсегда"
"sanction.permanent": " {grey}Навсегда",
"access.denied": "Это закрытый сервер: вас нет в его списке доступа"
}
3 changes: 2 additions & 1 deletion apps/player-management-css/src/lang/sv-SE.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"sanction.ungagged": " {green}Ditt chattförbud har hävts",
"sanction.reason": " {grey}Anledning: {0}",
"sanction.until": " {grey}Till {0}",
"sanction.permanent": " {grey}Permanent"
"sanction.permanent": " {grey}Permanent",
"access.denied": "Den här servern är privat: du finns inte på dess åtkomstlista"
}
3 changes: 2 additions & 1 deletion apps/player-management-css/src/lang/tr-TR.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"sanction.ungagged": " {green}Sohbet yasağınız kaldırıldı",
"sanction.reason": " {grey}Sebep: {0}",
"sanction.until": " {grey}Bitiş: {0}",
"sanction.permanent": " {grey}Kalıcı"
"sanction.permanent": " {grey}Kalıcı",
"access.denied": "Bu sunucu özeldir: erişim listesinde değilsiniz"
}
3 changes: 2 additions & 1 deletion apps/player-management-css/src/lang/uk-UA.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"sanction.ungagged": " {green}Заборону чату знято",
"sanction.reason": " {grey}Причина: {0}",
"sanction.until": " {grey}До {0}",
"sanction.permanent": " {grey}Назавжди"
"sanction.permanent": " {grey}Назавжди",
"access.denied": "Це приватний сервер: вас немає в його списку доступу"
}
Loading
Loading