diff --git a/Content.Client/RCD/RCDMenu.xaml.cs b/Content.Client/RCD/RCDMenu.xaml.cs index 51ec66ea444..3eb0397a690 100644 --- a/Content.Client/RCD/RCDMenu.xaml.cs +++ b/Content.Client/RCD/RCDMenu.xaml.cs @@ -68,7 +68,7 @@ public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui) tooltip = Loc.GetString(entProto.Name); } - tooltip = char.ToUpper(tooltip[0]) + tooltip.Remove(0, 1); + tooltip = OopsConcat(char.ToUpper(tooltip[0]).ToString(), tooltip.Remove(0, 1)); var button = new RCDMenuButton() { @@ -119,6 +119,12 @@ public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui) SendRCDSystemMessageAction += bui.SendRCDSystemMessage; } + private static string OopsConcat(string a, string b) + { + // This exists to prevent Roslyn being clever and compiling something that fails sandbox checks. + return a + b; + } + private void AddRCDMenuButtonOnClickActions(Control control) { var radialContainer = control as RadialContainer; diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs index b6ca9d95d0b..eab62702ffe 100644 --- a/Content.Server.Database/Model.cs +++ b/Content.Server.Database/Model.cs @@ -881,6 +881,7 @@ public enum ConnectionDenyReason : byte Whitelist = 1, Full = 2, Panic = 3, + Connected = 4, } public class ServerBanHit diff --git a/Content.Server/Administration/ServerApi.cs b/Content.Server/Administration/ServerApi.cs index 04fd38598fb..2f7bcbe48e2 100644 --- a/Content.Server/Administration/ServerApi.cs +++ b/Content.Server/Administration/ServerApi.cs @@ -67,7 +67,7 @@ void IPostInjectInit.PostInject() _sawmill = _logManager.GetSawmill("serverApi"); // Get - RegisterActorHandler(HttpMethod.Get, "/admin/info", InfoHandler); + RegisterHandler(HttpMethod.Get, "/admin/info", InfoHandler); //frontier - not sure why this action needs an actor RegisterHandler(HttpMethod.Get, "/admin/game_rules", GetGameRules); RegisterHandler(HttpMethod.Get, "/admin/presets", GetPresets); @@ -452,7 +452,7 @@ await context.RespondJsonAsync(new GameruleResponse /// /// Handles fetching information. /// - private async Task InfoHandler(IStatusHandlerContext context, Actor actor) + private async Task InfoHandler(IStatusHandlerContext context) //frontier - we had an actor here and never used it so we drop it for now until im compelled to re-add it { /* Information to display diff --git a/Content.Server/Connection/ConnectionManager.cs b/Content.Server/Connection/ConnectionManager.cs index cd89f48d49f..4b57aa66157 100644 --- a/Content.Server/Connection/ConnectionManager.cs +++ b/Content.Server/Connection/ConnectionManager.cs @@ -2,6 +2,8 @@ using System.Runtime.InteropServices; using System.Text.Json.Nodes; using System.Threading.Tasks; +using Content.Server._NF.Auth; +using Content.Server.Administration; using Content.Server.Database; using Content.Server.GameTicking; using Content.Server.Preferences.Managers; @@ -48,6 +50,9 @@ public sealed class ConnectionManager : IConnectionManager [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly ILogManager _logManager = default!; + //frontier + [Dependency] private readonly MiniAuthManager _authManager = default!; + private readonly Dictionary _temporaryBypasses = []; private ISawmill _sawmill = default!; @@ -228,6 +233,21 @@ private async Task NetMgrOnConnecting(NetConnectingArgs e) } } + //Frontier + //This is our little chunk that serves as a dAuth. It takes in a comma seperated list of IP:PORT, and chekcs + //the requesting player against the list of players logged in to other servers. It is intended to be failsafe. + //In the case of Admins, it shares the same bypass setting as the soft_max_player_limit + if (!_cfg.GetCVar(CCVars.AllowMultiConnect) && !adminBypass) + { + var serverListString = _cfg.GetCVar(CCVars.ServerAuthList); + var serverList = serverListString.Split(","); + foreach (var server in serverList) + { + if (await _authManager.IsPlayerConnected(server, userId)) + return (ConnectionDenyReason.Connected, Loc.GetString("multiauth-already-connected"), null); + } + } + // end Frontier return null; } diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index 3cdf3bfe8e2..4c43670a053 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -1,3 +1,4 @@ +using Content.Server._NF.Auth; using Content.Server.Acz; using Content.Server.Administration; using Content.Server.Administration.Logs; @@ -103,6 +104,7 @@ public override void Init() IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve(); _voteManager.Initialize(); _updateManager.Initialize(); diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index f6800f72890..8bd866a20d3 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -1,3 +1,4 @@ +using Content.Server._NF.Auth; using Content.Server.Administration; using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; @@ -61,6 +62,8 @@ public static void Register() IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); //Frontier + } } } diff --git a/Content.Server/_NF/Auth/Auth.cs b/Content.Server/_NF/Auth/Auth.cs new file mode 100644 index 00000000000..8ec682d07bc --- /dev/null +++ b/Content.Server/_NF/Auth/Auth.cs @@ -0,0 +1,88 @@ +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using System.Net.Http; +using System.Net.Http.Json; +using System.Net.Http.Headers; +using Content.Shared.CCVar; +using Robust.Shared.Configuration; +using JetBrains.Annotations; + +namespace Content.Server._NF.Auth; + +public sealed class MiniAuthManager +{ + [Dependency] private readonly IConfigurationManager _cfg = default!; + + private readonly HttpClient _http = new(); + + /// + /// Frontier function to ping a server and check to see if the given player is currently connected to the given server. + /// Servers using this function must share an admin_api token as defined in their respective server_config.toml + /// + /// The address of the server to ping. + /// the GUID of the player to check for connection. + /// True if the response from the server is successful and the player is connected. False in any case of error, timeout, or failure. + public async Task IsPlayerConnected(string address, Guid player) + { + var connected = false; + var statusAddress = "http://" + address + "/admin/info"; + + var cancel = new CancellationToken(); + var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancel); + linkedToken.CancelAfter(TimeSpan.FromSeconds(10)); + + _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("SS14Token", _cfg.GetCVar(CCVars.AdminApiToken)); + + //We need to do a try catch here because theres essentially no way to guarantee our json response is proper. + //Throughout all of this, we want it to fail to deny, not fail to allow, so if any step of our auth goes wrong, + //people can still connect. + try + { + var status = await _http.GetFromJsonAsync(statusAddress, linkedToken.Token); + + foreach (var connectedPlayer in status!.Players) + { + if (connectedPlayer.UserId == player) + { + connected = true; + break; + } + } + } + catch (Exception e) + { + } + return connected; + } + + /// + /// Record used to send the response for the info endpoint. + /// Frontier - This is a direct copy of ServerAPI.InfoResponse to match the json format. they kept it private so i just copied it + /// + [UsedImplicitly] + private sealed record InfoResponse + { + public required int RoundId { get; init; } + public required List Players { get; init; } + public required List GameRules { get; init; } + public required string? GamePreset { get; init; } + public required MapInfo? Map { get; init; } + public required string? MOTD { get; init; } + public required Dictionary PanicBunker { get; init; } + + public sealed class Player + { + public required Guid UserId { get; init; } + public required string Name { get; init; } + public required bool IsAdmin { get; init; } + public required bool IsDeadminned { get; init; } + } + + public sealed class MapInfo + { + public required string Id { get; init; } + public required string Name { get; init; } + } + } +} diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 4d9a704f521..a80567d0173 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -2045,6 +2045,11 @@ public static readonly CVarDef public static readonly CVarDef TippyEntity = CVarDef.Create("tippy.entity", "TippyClippy", CVar.SERVER | CVar.REPLICATED); // Frontier - Tippy ServerAuthList = + CVarDef.Create("frontier.auth_servers", "", CVar.CONFIDENTIAL | CVar.SERVERONLY); + + public static readonly CVarDef AllowMultiConnect = + CVarDef.Create("frontier.allow_multi_connect", true, CVar.CONFIDENTIAL | CVar.SERVERONLY); /* * DEBUG */ diff --git a/Content.Shared/Chat/SharedChatSystem.cs b/Content.Shared/Chat/SharedChatSystem.cs index e61d93efaeb..4f0b1465cd2 100644 --- a/Content.Shared/Chat/SharedChatSystem.cs +++ b/Content.Shared/Chat/SharedChatSystem.cs @@ -152,10 +152,16 @@ public string SanitizeMessageCapital(string message) if (string.IsNullOrEmpty(message)) return message; // Capitalize first letter - message = char.ToUpper(message[0]) + message.Remove(0, 1); + message = OopsConcat(char.ToUpper(message[0]).ToString(), message.Remove(0, 1)); return message; } + private static string OopsConcat(string a, string b) + { + // This exists to prevent Roslyn being clever and compiling something that fails sandbox checks. + return a + b; + } + public string SanitizeMessageCapitalizeTheWordI(string message, string theWordI = "i") { if (string.IsNullOrEmpty(message)) diff --git a/Resources/Locale/en-US/_NF/adventure/adventure.ftl b/Resources/Locale/en-US/_NF/adventure/adventure.ftl index 02872d0e3ae..a6fbf130949 100644 --- a/Resources/Locale/en-US/_NF/adventure/adventure.ftl +++ b/Resources/Locale/en-US/_NF/adventure/adventure.ftl @@ -21,6 +21,7 @@ shipyard-rules-default2 = shuttle-ftl-proximity = Nearby objects too massive for FTL! changelog-tab-title-Upstream = Upstream Changelog +multiauth-already-connected = Already connected to Frontier Official servers. public-transit-departure = Now departing for {$destination}. Estimated travel time: {$flytime} seconds. public-transit-arrival = Thank you for choosing NT Public Transit. Next transfer to {$destination} departs in {$waittime} seconds. \ No newline at end of file