Game server api (#23129)
* Stuff * Fix RoundStatus Action * Rename stuff * Also needs to run on the main thread * minor stuff * Move to not be an EntitySystem * Part 1 of making this less shit * Make some more stuff run on main thread * Actor check, logging and remove token from log. * Better log * Change ActionKick to use Guid instead of username * Fix build * Bandaid fix for test fails
This commit is contained in:
795
Content.Server/Administration/ServerAPI.cs
Normal file
795
Content.Server/Administration/ServerAPI.cs
Normal file
@@ -0,0 +1,795 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Content.Server.Administration.Systems;
|
||||||
|
using Content.Server.GameTicking;
|
||||||
|
using Content.Server.GameTicking.Presets;
|
||||||
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
|
using Content.Server.Maps;
|
||||||
|
using Content.Server.RoundEnd;
|
||||||
|
using Content.Shared.Administration.Managers;
|
||||||
|
using Content.Shared.CCVar;
|
||||||
|
using Content.Shared.Prototypes;
|
||||||
|
using Robust.Server.ServerStatus;
|
||||||
|
using Robust.Shared.Asynchronous;
|
||||||
|
using Robust.Shared.Configuration;
|
||||||
|
using Robust.Shared.Network;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Server.Administration;
|
||||||
|
|
||||||
|
public sealed class ServerApi : IPostInjectInit
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IStatusHost _statusHost = default!;
|
||||||
|
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||||
|
[Dependency] private readonly ISharedPlayerManager _playerManager = default!; // Players
|
||||||
|
[Dependency] private readonly ISharedAdminManager _adminManager = default!; // Admins
|
||||||
|
[Dependency] private readonly IGameMapManager _gameMapManager = default!; // Map name
|
||||||
|
[Dependency] private readonly IServerNetManager _netManager = default!; // Kick
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; // Game rules
|
||||||
|
[Dependency] private readonly IComponentFactory _componentFactory = default!; // Needed to circumvent the "IoC has no context on this thread" error until I figure out how to do it properly
|
||||||
|
[Dependency] private readonly ITaskManager _taskManager = default!; // game explodes when calling stuff from the non-game thread
|
||||||
|
[Dependency] private readonly EntityManager _entityManager = default!;
|
||||||
|
|
||||||
|
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||||
|
|
||||||
|
private string _token = default!;
|
||||||
|
private ISawmill _sawmill = default!;
|
||||||
|
private string _motd = default!;
|
||||||
|
|
||||||
|
public void PostInject()
|
||||||
|
{
|
||||||
|
_sawmill = Logger.GetSawmill("serverApi");
|
||||||
|
|
||||||
|
// Get
|
||||||
|
_statusHost.AddHandler(InfoHandler);
|
||||||
|
_statusHost.AddHandler(GetGameRules);
|
||||||
|
_statusHost.AddHandler(GetForcePresets);
|
||||||
|
|
||||||
|
// Post
|
||||||
|
_statusHost.AddHandler(ActionRoundStatus);
|
||||||
|
_statusHost.AddHandler(ActionKick);
|
||||||
|
_statusHost.AddHandler(ActionAddGameRule);
|
||||||
|
_statusHost.AddHandler(ActionEndGameRule);
|
||||||
|
_statusHost.AddHandler(ActionForcePreset);
|
||||||
|
_statusHost.AddHandler(ActionForceMotd);
|
||||||
|
_statusHost.AddHandler(ActionPanicPunker);
|
||||||
|
|
||||||
|
// Bandaid fix for the test fails
|
||||||
|
// "System.Collections.Generic.KeyNotFoundException : The given key 'server.admin_api_token' was not present in the dictionary."
|
||||||
|
// TODO: Figure out why this happens
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_config.OnValueChanged(CCVars.AdminApiToken, UpdateToken, true);
|
||||||
|
_config.OnValueChanged(CCVars.MOTD, UpdateMotd, true);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_sawmill.Error("Failed to subscribe to config vars: {0}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Shutdown()
|
||||||
|
{
|
||||||
|
_config.UnsubValueChanged(CCVars.AdminApiToken, UpdateToken);
|
||||||
|
_config.UnsubValueChanged(CCVars.MOTD, UpdateMotd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateToken(string token)
|
||||||
|
{
|
||||||
|
_token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateMotd(string motd)
|
||||||
|
{
|
||||||
|
_motd = motd;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#region Actions
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Changes the panic bunker settings.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> ActionPanicPunker(IStatusHandlerContext context)
|
||||||
|
{
|
||||||
|
if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/admin/actions/panic_bunker")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckAccess(context))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckActor(context, out var actor))
|
||||||
|
{
|
||||||
|
await context.RespondAsync("An actor is required to perform this action.", HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionSupplied = context.RequestHeaders.TryGetValue("Action", out var action);
|
||||||
|
if (!actionSupplied)
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var valueSupplied = context.RequestHeaders.TryGetValue("Value", out var value);
|
||||||
|
if (!valueSupplied)
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action) // TODO: This looks bad, there has to be a better way to do this.
|
||||||
|
{
|
||||||
|
case "enabled":
|
||||||
|
if (!bool.TryParse(value.ToString(), out var enabled))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_taskManager.RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
_config.SetCVar(CCVars.PanicBunkerEnabled, enabled);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "disable_with_admins":
|
||||||
|
if (!bool.TryParse(value.ToString(), out var disableWithAdmins))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_taskManager.RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
_config.SetCVar(CCVars.PanicBunkerDisableWithAdmins, disableWithAdmins);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "enable_without_admins":
|
||||||
|
if (!bool.TryParse(value.ToString(), out var enableWithoutAdmins))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_taskManager.RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
_config.SetCVar(CCVars.PanicBunkerEnableWithoutAdmins, enableWithoutAdmins);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "count_deadminned_admins":
|
||||||
|
if (!bool.TryParse(value.ToString(), out var countDeadminnedAdmins))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_taskManager.RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
_config.SetCVar(CCVars.PanicBunkerCountDeadminnedAdmins, countDeadminnedAdmins);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "show_reason":
|
||||||
|
if (!bool.TryParse(value.ToString(), out var showReason))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_taskManager.RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
_config.SetCVar(CCVars.PanicBunkerShowReason, showReason);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "min_account_age_hours":
|
||||||
|
if (!int.TryParse(value.ToString(), out var minAccountAgeHours))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_taskManager.RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
_config.SetCVar(CCVars.PanicBunkerMinAccountAge, minAccountAgeHours * 60);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "min_overall_hours":
|
||||||
|
if (!int.TryParse(value.ToString(), out var minOverallHours))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_taskManager.RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
_config.SetCVar(CCVars.PanicBunkerMinOverallHours, minOverallHours * 60);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_sawmill.Info($"Panic bunker setting {action} changed to {value} by {actor!.Name}({actor!.Guid}).");
|
||||||
|
await context.RespondAsync("Success", HttpStatusCode.OK);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the current MOTD.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> ActionForceMotd(IStatusHandlerContext context)
|
||||||
|
{
|
||||||
|
if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/admin/actions/set_motd")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckAccess(context))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckActor(context, out var actor))
|
||||||
|
{
|
||||||
|
await context.RespondAsync("An actor is required to perform this action.", HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var motdSupplied = context.RequestHeaders.TryGetValue("MOTD", out var motd);
|
||||||
|
if (!motdSupplied)
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_sawmill.Info($"MOTD changed to \"{motd}\" by {actor!.Name}({actor!.Guid}).");
|
||||||
|
|
||||||
|
_taskManager.RunOnMainThread(() => _config.SetCVar(CCVars.MOTD, motd.ToString()));
|
||||||
|
// A hook in the MOTD system sends the changes to each client
|
||||||
|
await context.RespondAsync("Success", HttpStatusCode.OK);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forces the next preset-
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> ActionForcePreset(IStatusHandlerContext context)
|
||||||
|
{
|
||||||
|
if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/admin/actions/force_preset")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckAccess(context))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckActor(context, out var actor))
|
||||||
|
{
|
||||||
|
await context.RespondAsync("An actor is required to perform this action.", HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ticker = _entitySystemManager.GetEntitySystem<GameTicker>();
|
||||||
|
|
||||||
|
if (ticker.RunLevel != GameRunLevel.PreRoundLobby)
|
||||||
|
{
|
||||||
|
_sawmill.Info($"Attempted to force preset {actor!.Name}({actor!.Guid}) while the game was not in the pre-round lobby.");
|
||||||
|
await context.RespondAsync("This can only be executed while the game is in the pre-round lobby.", HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var presetSupplied = context.RequestHeaders.TryGetValue("PresetId", out var preset);
|
||||||
|
if (!presetSupplied)
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await RunOnMainThread(() => ticker.FindGamePreset(preset.ToString()));
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
await context.RespondAsync($"No preset exists with name {preset}.", HttpStatusCode.NotFound);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_taskManager.RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
ticker.SetGamePreset(result);
|
||||||
|
});
|
||||||
|
_sawmill.Info($"Forced the game to start with preset {preset} by {actor!.Name}({actor!.Guid}).");
|
||||||
|
await context.RespondAsync("Success", HttpStatusCode.OK);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ends an active game rule.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> ActionEndGameRule(IStatusHandlerContext context)
|
||||||
|
{
|
||||||
|
if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/admin/actions/end_game_rule")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckAccess(context))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckActor(context, out var actor))
|
||||||
|
{
|
||||||
|
await context.RespondAsync("An actor is required to perform this action.", HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gameRuleSupplied = context.RequestHeaders.TryGetValue("GameRuleId", out var gameRule);
|
||||||
|
if (!gameRuleSupplied)
|
||||||
|
{
|
||||||
|
_sawmill.Info($"Attempted to end game rule without supplying a game rule name by {actor!.Name}({actor!.Guid}).");
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var ticker = _entitySystemManager.GetEntitySystem<GameTicker>();
|
||||||
|
var gameRuleEntity = await RunOnMainThread(() => ticker
|
||||||
|
.GetActiveGameRules()
|
||||||
|
.FirstOrNull(rule => _entityManager.MetaQuery.GetComponent(rule).EntityPrototype?.ID == gameRule.ToString()));
|
||||||
|
|
||||||
|
if (gameRuleEntity == null) // Game rule not found
|
||||||
|
{
|
||||||
|
_sawmill.Info($"Attempted to end game rule {gameRule} by {actor!.Name}({actor!.Guid}), but it was not found.");
|
||||||
|
await context.RespondAsync("Gamerule not found or not active",HttpStatusCode.NotFound);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_sawmill.Info($"Ended game rule {gameRule} by {actor!.Name}({actor!.Guid}).");
|
||||||
|
_taskManager.RunOnMainThread(() => ticker.EndGameRule((EntityUid) gameRuleEntity));
|
||||||
|
await context.RespondAsync($"Ended game rule {gameRule}({gameRuleEntity})", HttpStatusCode.OK);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a game rule to the current round.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> ActionAddGameRule(IStatusHandlerContext context)
|
||||||
|
{
|
||||||
|
if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/admin/actions/add_game_rule")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckAccess(context))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckActor(context, out var actor))
|
||||||
|
{
|
||||||
|
await context.RespondAsync("An actor is required to perform this action.", HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gameRuleSupplied = context.RequestHeaders.TryGetValue("GameRuleId", out var gameRule);
|
||||||
|
if (!gameRuleSupplied)
|
||||||
|
{
|
||||||
|
_sawmill.Info($"Attempted to add game rule without supplying a game rule name by {actor!.Name}({actor!.Guid}).");
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ticker = _entitySystemManager.GetEntitySystem<GameTicker>();
|
||||||
|
|
||||||
|
var tsc = new TaskCompletionSource<EntityUid>();
|
||||||
|
_taskManager.RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
var ruleEntity = ticker.AddGameRule(gameRule.ToString());
|
||||||
|
_sawmill.Info($"Added game rule {gameRule} by {actor!.Name}({actor!.Guid}).");
|
||||||
|
if (ticker.RunLevel == GameRunLevel.InRound)
|
||||||
|
{
|
||||||
|
ticker.StartGameRule(ruleEntity);
|
||||||
|
_sawmill.Info($"Started game rule {gameRule} by {actor!.Name}({actor!.Guid}).");
|
||||||
|
}
|
||||||
|
tsc.TrySetResult(ruleEntity);
|
||||||
|
});
|
||||||
|
|
||||||
|
var ruleEntity = await tsc.Task;
|
||||||
|
await context.RespondAsync($"Added game rule {ruleEntity}", HttpStatusCode.OK);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kicks a player.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> ActionKick(IStatusHandlerContext context)
|
||||||
|
{
|
||||||
|
if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/admin/actions/kick")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckAccess(context))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckActor(context, out var actor))
|
||||||
|
{
|
||||||
|
await context.RespondAsync("An actor is required to perform this action.", HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var playerSupplied = context.RequestHeaders.TryGetValue("Guid", out var guid);
|
||||||
|
if (!playerSupplied)
|
||||||
|
{
|
||||||
|
_sawmill.Info($"Attempted to kick player without supplying a username by {actor!.Name}({actor!.Guid}).");
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var session = await RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
// There is no function to get a session by GUID, so we have to iterate over all sessions and check their GUIDs.
|
||||||
|
foreach (var player in _playerManager.Sessions)
|
||||||
|
{
|
||||||
|
if (player.UserId.UserId.ToString() == guid.ToString())
|
||||||
|
{
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (session == null)
|
||||||
|
{
|
||||||
|
_sawmill.Info($"Attempted to kick player {guid} by {actor!.Name}({actor!.Guid}), but they were not found.");
|
||||||
|
await context.RespondAsync("Player not found", HttpStatusCode.NotFound);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reasonSupplied = context.RequestHeaders.TryGetValue("Reason", out var reason);
|
||||||
|
if (!reasonSupplied)
|
||||||
|
{
|
||||||
|
reason = "No reason supplied";
|
||||||
|
}
|
||||||
|
|
||||||
|
reason += " (kicked by admin)";
|
||||||
|
|
||||||
|
_taskManager.RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
_netManager.DisconnectChannel(session.Channel, reason.ToString());
|
||||||
|
});
|
||||||
|
await context.RespondAsync("Success", HttpStatusCode.OK);
|
||||||
|
_sawmill.Info("Kicked player {0} ({1}) for {2} by {3}({4})", session.Name, session.UserId.UserId.ToString(), reason, actor!.Name, actor!.Guid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round restart/end
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> ActionRoundStatus(IStatusHandlerContext context)
|
||||||
|
{
|
||||||
|
if (context.RequestMethod != HttpMethod.Post || context.Url!.AbsolutePath != "/admin/actions/round")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckAccess(context))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckActor(context, out var actor))
|
||||||
|
{
|
||||||
|
await context.RespondAsync("An actor is required to perform this action.", HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not using body, because that's a stream and I don't want to deal with that
|
||||||
|
var actionSupplied = context.RequestHeaders.TryGetValue("Action", out var action);
|
||||||
|
if (!actionSupplied)
|
||||||
|
{
|
||||||
|
_sawmill.Info($"Attempted to {action} round without supplying an action by {actor!.Name}({actor!.Guid}).");
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ticker = _entitySystemManager.GetEntitySystem<GameTicker>();
|
||||||
|
var roundEndSystem = _entitySystemManager.GetEntitySystem<RoundEndSystem>();
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case "start":
|
||||||
|
if (ticker.RunLevel != GameRunLevel.PreRoundLobby)
|
||||||
|
{
|
||||||
|
await context.RespondAsync("Round already started", HttpStatusCode.BadRequest);
|
||||||
|
_sawmill.Info("Forced round start failed: round already started");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_taskManager.RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
ticker.StartRound();
|
||||||
|
});
|
||||||
|
_sawmill.Info("Forced round start");
|
||||||
|
break;
|
||||||
|
case "end":
|
||||||
|
if (ticker.RunLevel != GameRunLevel.InRound)
|
||||||
|
{
|
||||||
|
await context.RespondAsync("Round already ended", HttpStatusCode.BadRequest);
|
||||||
|
_sawmill.Info("Forced round end failed: round is not in progress");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_taskManager.RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
roundEndSystem.EndRound();
|
||||||
|
});
|
||||||
|
_sawmill.Info("Forced round end");
|
||||||
|
break;
|
||||||
|
case "restart":
|
||||||
|
if (ticker.RunLevel != GameRunLevel.InRound)
|
||||||
|
{
|
||||||
|
await context.RespondAsync("Round already ended", HttpStatusCode.BadRequest);
|
||||||
|
_sawmill.Info("Forced round restart failed: round is not in progress");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_taskManager.RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
roundEndSystem.EndRound();
|
||||||
|
});
|
||||||
|
_sawmill.Info("Forced round restart");
|
||||||
|
break;
|
||||||
|
case "restartnow": // You should restart yourself NOW!!!
|
||||||
|
_taskManager.RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
ticker.RestartRound();
|
||||||
|
});
|
||||||
|
_sawmill.Info("Forced instant round restart");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_sawmill.Info($"Round {action} by {actor!.Name}({actor!.Guid}).");
|
||||||
|
await context.RespondAsync("Success", HttpStatusCode.OK);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Fetching
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an array containing all presets.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> GetForcePresets(IStatusHandlerContext context)
|
||||||
|
{
|
||||||
|
if (context.RequestMethod != HttpMethod.Get || context.Url!.AbsolutePath != "/admin/force_presets")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckAccess(context))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var jObject = new JsonObject();
|
||||||
|
var presets = new List<string>();
|
||||||
|
foreach (var preset in _prototypeManager.EnumeratePrototypes<GamePresetPrototype>())
|
||||||
|
{
|
||||||
|
presets.Add(preset.ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
jObject["presets"] = JsonNode.Parse(JsonSerializer.Serialize(presets));
|
||||||
|
await context.RespondAsync(jObject.ToString(), HttpStatusCode.OK);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an array containing all game rules.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> GetGameRules(IStatusHandlerContext context)
|
||||||
|
{
|
||||||
|
if (context.RequestMethod != HttpMethod.Get || context.Url!.AbsolutePath != "/admin/game_rules")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckAccess(context))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var jObject = new JsonObject();
|
||||||
|
var gameRules = new List<string>();
|
||||||
|
foreach (var gameRule in _prototypeManager.EnumeratePrototypes<EntityPrototype>())
|
||||||
|
{
|
||||||
|
if (gameRule.Abstract)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (gameRule.HasComponent<GameRuleComponent>(_componentFactory))
|
||||||
|
gameRules.Add(gameRule.ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
jObject["game_rules"] = JsonNode.Parse(JsonSerializer.Serialize(gameRules));
|
||||||
|
await context.RespondAsync(jObject.ToString(), HttpStatusCode.OK);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles fetching information.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> InfoHandler(IStatusHandlerContext context)
|
||||||
|
{
|
||||||
|
if (context.RequestMethod != HttpMethod.Get || context.Url!.AbsolutePath != "/admin/info" || _token == string.Empty)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
// 404
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckAccess(context))
|
||||||
|
{
|
||||||
|
await context.RespondErrorAsync(HttpStatusCode.Unauthorized);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckActor(context, out var actor))
|
||||||
|
{
|
||||||
|
await context.RespondAsync("An actor is required to perform this action.", HttpStatusCode.BadRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Information to display
|
||||||
|
Round number
|
||||||
|
Connected players
|
||||||
|
Active admins
|
||||||
|
Active game rules
|
||||||
|
Active game preset
|
||||||
|
Active map
|
||||||
|
MOTD
|
||||||
|
Panic bunker status
|
||||||
|
*/
|
||||||
|
|
||||||
|
var ticker = _entitySystemManager.GetEntitySystem<GameTicker>();
|
||||||
|
var adminSystem = _entitySystemManager.GetEntitySystem<AdminSystem>();
|
||||||
|
|
||||||
|
var jObject = new JsonObject();
|
||||||
|
|
||||||
|
jObject["round_id"] = await RunOnMainThread(() => ticker.RoundId);
|
||||||
|
|
||||||
|
var players = new List<Player>();
|
||||||
|
var onlineAdmins = new List<Player>();
|
||||||
|
var onlineAdminsDeadmined = new List<Player>();
|
||||||
|
|
||||||
|
foreach (var player in _playerManager.Sessions)
|
||||||
|
{
|
||||||
|
players.Add(new Player
|
||||||
|
{
|
||||||
|
Guid = player.UserId.UserId.ToString(),
|
||||||
|
Name = player.Name
|
||||||
|
});
|
||||||
|
if (await RunOnMainThread(() => _adminManager.IsAdmin(player)))
|
||||||
|
{
|
||||||
|
onlineAdmins.Add(new Player
|
||||||
|
{
|
||||||
|
Guid = player.UserId.UserId.ToString(),
|
||||||
|
Name = player.Name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (await RunOnMainThread(() => _adminManager.IsAdmin(player, true)))
|
||||||
|
{
|
||||||
|
onlineAdminsDeadmined.Add(new Player
|
||||||
|
{
|
||||||
|
Guid = player.UserId.UserId.ToString(),
|
||||||
|
Name = player.Name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The JsonSerializer.Serialize into JsonNode.Parse is a bit of a hack
|
||||||
|
jObject["players"] = JsonNode.Parse(JsonSerializer.Serialize(players));
|
||||||
|
jObject["admins"] = JsonNode.Parse(JsonSerializer.Serialize(onlineAdmins));
|
||||||
|
jObject["deadmined"] = JsonNode.Parse(JsonSerializer.Serialize(onlineAdminsDeadmined));
|
||||||
|
|
||||||
|
var gameRules = new List<string>();
|
||||||
|
foreach (var addedGameRule in await RunOnMainThread(() => ticker.GetActiveGameRules()))
|
||||||
|
{
|
||||||
|
var meta = _entityManager.MetaQuery.GetComponent(addedGameRule);
|
||||||
|
gameRules.Add(meta.EntityPrototype?.ID ?? meta.EntityPrototype?.Name ?? "Unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
jObject["game_rules"] = JsonNode.Parse(JsonSerializer.Serialize(gameRules));
|
||||||
|
jObject["game_preset"] = ticker.CurrentPreset?.ID;
|
||||||
|
jObject["map"] = await RunOnMainThread(() => _gameMapManager.GetSelectedMap()?.MapName ?? "Unknown");
|
||||||
|
jObject["motd"] = _motd;
|
||||||
|
jObject["panic_bunker"] = new JsonObject();
|
||||||
|
jObject["panic_bunker"]!["enabled"] = adminSystem.PanicBunker.Enabled;
|
||||||
|
jObject["panic_bunker"]!["disable_with_admins"] = adminSystem.PanicBunker.DisableWithAdmins;
|
||||||
|
jObject["panic_bunker"]!["enable_without_admins"] = adminSystem.PanicBunker.EnableWithoutAdmins;
|
||||||
|
jObject["panic_bunker"]!["count_deadminned_admins"] = adminSystem.PanicBunker.CountDeadminnedAdmins;
|
||||||
|
jObject["panic_bunker"]!["show_reason"] = adminSystem.PanicBunker.ShowReason;
|
||||||
|
jObject["panic_bunker"]!["min_account_age_hours"] = adminSystem.PanicBunker.MinAccountAgeHours;
|
||||||
|
jObject["panic_bunker"]!["min_overall_hours"] = adminSystem.PanicBunker.MinOverallHours;
|
||||||
|
|
||||||
|
_sawmill.Info($"Info requested by {actor!.Name}({actor!.Guid}).");
|
||||||
|
await context.RespondAsync(jObject.ToString(), HttpStatusCode.OK);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private bool CheckAccess(IStatusHandlerContext context)
|
||||||
|
{
|
||||||
|
var auth = context.RequestHeaders.TryGetValue("Authorization", out var authToken);
|
||||||
|
if (!auth)
|
||||||
|
{
|
||||||
|
_sawmill.Info(@"Unauthorized access attempt to admin API. No auth header");
|
||||||
|
return false;
|
||||||
|
} // No auth header, no access
|
||||||
|
|
||||||
|
if (authToken == _token)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Invalid auth header, no access
|
||||||
|
_sawmill.Info(@"Unauthorized access attempt to admin API. ""{0}""", authToken.ToString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Async helper function which runs a task on the main thread and returns the result.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<T> RunOnMainThread<T>(Func<T> func)
|
||||||
|
{
|
||||||
|
var taskCompletionSource = new TaskCompletionSource<T>();
|
||||||
|
_taskManager.RunOnMainThread(() =>
|
||||||
|
{
|
||||||
|
taskCompletionSource.TrySetResult(func());
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await taskCompletionSource.Task;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CheckActor(IStatusHandlerContext context, out Player? actor)
|
||||||
|
{
|
||||||
|
// We are trusting the header to be correct.
|
||||||
|
// This is fine because the header is set by the SS14.Admin backend.
|
||||||
|
var actorSupplied = context.RequestHeaders.TryGetValue("Actor", out var actorHeader);
|
||||||
|
if (!actorSupplied)
|
||||||
|
{
|
||||||
|
actor = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stringRep = actorHeader.ToString();
|
||||||
|
actor = new Player()
|
||||||
|
{
|
||||||
|
// GUID_NAME format
|
||||||
|
Guid = stringRep[..stringRep.IndexOf('_')],
|
||||||
|
Name = stringRep[(stringRep.IndexOf('_') + 1)..]
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Player
|
||||||
|
{
|
||||||
|
public string Guid { get; set; } = default!;
|
||||||
|
public string Name { get; set; } = default!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ namespace Content.Server.Administration.Systems
|
|||||||
public IReadOnlySet<NetUserId> RoundActivePlayers => _roundActivePlayers;
|
public IReadOnlySet<NetUserId> RoundActivePlayers => _roundActivePlayers;
|
||||||
|
|
||||||
private readonly HashSet<NetUserId> _roundActivePlayers = new();
|
private readonly HashSet<NetUserId> _roundActivePlayers = new();
|
||||||
private readonly PanicBunkerStatus _panicBunker = new();
|
public readonly PanicBunkerStatus PanicBunker = new();
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -248,7 +248,7 @@ namespace Content.Server.Administration.Systems
|
|||||||
|
|
||||||
private void OnPanicBunkerChanged(bool enabled)
|
private void OnPanicBunkerChanged(bool enabled)
|
||||||
{
|
{
|
||||||
_panicBunker.Enabled = enabled;
|
PanicBunker.Enabled = enabled;
|
||||||
_chat.SendAdminAlert(Loc.GetString(enabled
|
_chat.SendAdminAlert(Loc.GetString(enabled
|
||||||
? "admin-ui-panic-bunker-enabled-admin-alert"
|
? "admin-ui-panic-bunker-enabled-admin-alert"
|
||||||
: "admin-ui-panic-bunker-disabled-admin-alert"
|
: "admin-ui-panic-bunker-disabled-admin-alert"
|
||||||
@@ -259,52 +259,52 @@ namespace Content.Server.Administration.Systems
|
|||||||
|
|
||||||
private void OnPanicBunkerDisableWithAdminsChanged(bool enabled)
|
private void OnPanicBunkerDisableWithAdminsChanged(bool enabled)
|
||||||
{
|
{
|
||||||
_panicBunker.DisableWithAdmins = enabled;
|
PanicBunker.DisableWithAdmins = enabled;
|
||||||
UpdatePanicBunker();
|
UpdatePanicBunker();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPanicBunkerEnableWithoutAdminsChanged(bool enabled)
|
private void OnPanicBunkerEnableWithoutAdminsChanged(bool enabled)
|
||||||
{
|
{
|
||||||
_panicBunker.EnableWithoutAdmins = enabled;
|
PanicBunker.EnableWithoutAdmins = enabled;
|
||||||
UpdatePanicBunker();
|
UpdatePanicBunker();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPanicBunkerCountDeadminnedAdminsChanged(bool enabled)
|
private void OnPanicBunkerCountDeadminnedAdminsChanged(bool enabled)
|
||||||
{
|
{
|
||||||
_panicBunker.CountDeadminnedAdmins = enabled;
|
PanicBunker.CountDeadminnedAdmins = enabled;
|
||||||
UpdatePanicBunker();
|
UpdatePanicBunker();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnShowReasonChanged(bool enabled)
|
private void OnShowReasonChanged(bool enabled)
|
||||||
{
|
{
|
||||||
_panicBunker.ShowReason = enabled;
|
PanicBunker.ShowReason = enabled;
|
||||||
SendPanicBunkerStatusAll();
|
SendPanicBunkerStatusAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPanicBunkerMinAccountAgeChanged(int minutes)
|
private void OnPanicBunkerMinAccountAgeChanged(int minutes)
|
||||||
{
|
{
|
||||||
_panicBunker.MinAccountAgeHours = minutes / 60;
|
PanicBunker.MinAccountAgeHours = minutes / 60;
|
||||||
SendPanicBunkerStatusAll();
|
SendPanicBunkerStatusAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPanicBunkerMinOverallHoursChanged(int hours)
|
private void OnPanicBunkerMinOverallHoursChanged(int hours)
|
||||||
{
|
{
|
||||||
_panicBunker.MinOverallHours = hours;
|
PanicBunker.MinOverallHours = hours;
|
||||||
SendPanicBunkerStatusAll();
|
SendPanicBunkerStatusAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdatePanicBunker()
|
private void UpdatePanicBunker()
|
||||||
{
|
{
|
||||||
var admins = _panicBunker.CountDeadminnedAdmins
|
var admins = PanicBunker.CountDeadminnedAdmins
|
||||||
? _adminManager.AllAdmins
|
? _adminManager.AllAdmins
|
||||||
: _adminManager.ActiveAdmins;
|
: _adminManager.ActiveAdmins;
|
||||||
var hasAdmins = admins.Any();
|
var hasAdmins = admins.Any();
|
||||||
|
|
||||||
if (hasAdmins && _panicBunker.DisableWithAdmins)
|
if (hasAdmins && PanicBunker.DisableWithAdmins)
|
||||||
{
|
{
|
||||||
_config.SetCVar(CCVars.PanicBunkerEnabled, false);
|
_config.SetCVar(CCVars.PanicBunkerEnabled, false);
|
||||||
}
|
}
|
||||||
else if (!hasAdmins && _panicBunker.EnableWithoutAdmins)
|
else if (!hasAdmins && PanicBunker.EnableWithoutAdmins)
|
||||||
{
|
{
|
||||||
_config.SetCVar(CCVars.PanicBunkerEnabled, true);
|
_config.SetCVar(CCVars.PanicBunkerEnabled, true);
|
||||||
}
|
}
|
||||||
@@ -314,7 +314,7 @@ namespace Content.Server.Administration.Systems
|
|||||||
|
|
||||||
private void SendPanicBunkerStatusAll()
|
private void SendPanicBunkerStatusAll()
|
||||||
{
|
{
|
||||||
var ev = new PanicBunkerChangedEvent(_panicBunker);
|
var ev = new PanicBunkerChangedEvent(PanicBunker);
|
||||||
foreach (var admin in _adminManager.AllAdmins)
|
foreach (var admin in _adminManager.AllAdmins)
|
||||||
{
|
{
|
||||||
RaiseNetworkEvent(ev, admin);
|
RaiseNetworkEvent(ev, admin);
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ namespace Content.Server.IoC
|
|||||||
IoCManager.Register<PoissonDiskSampler>();
|
IoCManager.Register<PoissonDiskSampler>();
|
||||||
IoCManager.Register<DiscordWebhook>();
|
IoCManager.Register<DiscordWebhook>();
|
||||||
IoCManager.Register<ServerDbEntryManager>();
|
IoCManager.Register<ServerDbEntryManager>();
|
||||||
|
IoCManager.Register<ServerApi>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ namespace Content.Shared.CCVar
|
|||||||
public static readonly CVarDef<string> RulesHeader =
|
public static readonly CVarDef<string> RulesHeader =
|
||||||
CVarDef.Create("server.rules_header", "ui-rules-header", CVar.REPLICATED | CVar.SERVER);
|
CVarDef.Create("server.rules_header", "ui-rules-header", CVar.REPLICATED | CVar.SERVER);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The token used to authenticate with the admin API. Leave empty to disable the admin API. This is a secret! Do not share!
|
||||||
|
/// </summary>
|
||||||
|
public static readonly CVarDef<string> AdminApiToken =
|
||||||
|
CVarDef.Create("server.admin_api_token", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Ambience
|
* Ambience
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user