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