diff --git a/Robust.Client/BaseClient.cs b/Robust.Client/BaseClient.cs
index 5756c24ceb9..94bf1d69a30 100644
--- a/Robust.Client/BaseClient.cs
+++ b/Robust.Client/BaseClient.cs
@@ -31,6 +31,7 @@ public sealed class BaseClient : IBaseClient, IPostInjectInit
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IClientGameStateManager _gameStates = default!;
[Dependency] private readonly ILogManager _logMan = default!;
+ [Dependency] private readonly IHttpManagerInternal _http = default!;
///
public ushort DefaultPort { get; } = 1212;
@@ -255,6 +256,7 @@ private void GameStartedSetup()
private void GameStoppedReset()
{
+ _http.Shutdown();
_configManager.FlushMessages();
_gameStates.Reset();
_playMan.Shutdown();
diff --git a/Robust.Server/BaseServer.cs b/Robust.Server/BaseServer.cs
index 6fdaadc1909..c1994082423 100644
--- a/Robust.Server/BaseServer.cs
+++ b/Robust.Server/BaseServer.cs
@@ -36,11 +36,11 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Replays;
-using Robust.Shared.Toolshed;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
+using Robust.Shared.Toolshed;
using Robust.Shared.Upload;
using Robust.Shared.Utility;
using Serilog.Debugging;
@@ -111,6 +111,7 @@ internal sealed class BaseServer : IBaseServerInternal, IPostInjectInit
[Dependency] private readonly IReflectionManager _refMan = default!;
[Dependency] private readonly ITransferManager _transfer = default!;
[Dependency] private readonly ServerTransferTestManager _transferTest = default!;
+ [Dependency] private readonly IHttpManagerInternal _http = default!;
private readonly Stopwatch _uptimeStopwatch = new();
@@ -675,6 +676,7 @@ private bool CheckIfShouldAutoPause()
// called right before main loop returns, do all saving/cleanup in here
public void Cleanup()
{
+ _http.Shutdown();
_replay.Shutdown();
_modLoader.Shutdown();
diff --git a/Robust.Shared.Tests/Networking/HttpManagerTest.cs b/Robust.Shared.Tests/Networking/HttpManagerTest.cs
new file mode 100644
index 00000000000..889d842e4e6
--- /dev/null
+++ b/Robust.Shared.Tests/Networking/HttpManagerTest.cs
@@ -0,0 +1,39 @@
+using NUnit.Framework;
+using Robust.Shared.Network;
+
+namespace Robust.Shared.Tests.Networking;
+
+[TestFixture]
+[TestOf(typeof(HttpManager))]
+public sealed class HttpManagerTest
+{
+ [Test]
+ [TestCase("http://www.google.com")]
+ [TestCase("https://www.google.com")]
+ [TestCase("http://google.com")]
+ [TestCase("https://google.com")]
+ public void TestNoError(string url)
+ {
+ var manager = new HttpManager();
+ var uri = new Uri(url);
+ Assert.DoesNotThrowAsync(() => manager.GetStreamAsync(uri));
+ Assert.DoesNotThrowAsync(() => manager.GetStringAsync(uri));
+ }
+
+ [Test]
+ [TestCase("http://192.168.0.0")]
+ [TestCase("http://192.168.0.1")]
+ [TestCase("http://192.168.255.255")]
+ [TestCase("http://10.0.0.0")]
+ [TestCase("http://10.255.255.255")]
+ [TestCase("http://172.16.0.0")]
+ [TestCase("http://172.31.255.255")]
+ public void TestLocalIPError(string url)
+ {
+ var manager = new HttpManager();
+ var uri = new Uri(url);
+ Assert.ThrowsAsync(() => manager.GetStreamAsync(uri));
+ Assert.ThrowsAsync(() => manager.GetStringAsync(uri));
+ Assert.ThrowsAsync(() => manager.GetFromJsonAsync(uri));
+ }
+}
diff --git a/Robust.Shared/Network/HttpManager.cs b/Robust.Shared/Network/HttpManager.cs
new file mode 100644
index 00000000000..e607a1df087
--- /dev/null
+++ b/Robust.Shared/Network/HttpManager.cs
@@ -0,0 +1,114 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Runtime.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Robust.Shared.Utility;
+
+namespace Robust.Shared.Network;
+
+public sealed class HttpManager : IHttpManagerInternal
+{
+ private readonly HttpClient _client = new();
+
+ void IHttpManagerInternal.Shutdown()
+ {
+ _client.CancelPendingRequests();
+ }
+
+ public async Task GetStreamAsync(Uri uri, CancellationToken cancel = default)
+ {
+ // Uri can be inherited which means it could be inherited by the user
+ // Am I going to check if that means they could modify it after the
+ // local address check?
+ // No, so we copy the original string instead just in case
+ // !!FUN!!
+ uri = new Uri(uri.OriginalString);
+ await ThrowIfLocalUri(uri);
+ return await _client.GetStreamAsync(uri, cancel);
+ }
+
+ public async Task GetStringAsync(Uri uri, CancellationToken cancel = default)
+ {
+ uri = new Uri(uri.OriginalString);
+ await ThrowIfLocalUri(uri);
+ return await _client.GetStringAsync(uri, cancel);
+ }
+
+ public async Task GetFromJsonAsync(Uri uri, CancellationToken cancel = default)
+ {
+ uri = new Uri(uri.OriginalString);
+ await ThrowIfLocalUri(uri);
+ return await _client.GetFromJsonAsync(uri, cancel);
+ }
+
+ private async Task ThrowIfLocalUri(Uri uri)
+ {
+ if (IPAddress.TryParse(uri.Host, out var ip))
+ ThrowIfLocalIP(ip);
+
+ var addresses = await Dns.GetHostAddressesAsync(uri.Host);
+ foreach (var dnsIP in addresses)
+ {
+ ThrowIfLocalIP(dnsIP);
+ }
+ }
+
+ private void ThrowIfLocalIP(IPAddress ip)
+ {
+ // IPv4
+ var ipv4 = ip.ToString()
+ .Split(".")
+ .Select(s => (int?) (int.TryParse(s, out var i) ? i : null))
+ .Where(i => i != null)
+ .ToArray();
+ ipv4.TryGetValue(0, out var first);
+ ipv4.TryGetValue(1, out var second);
+ switch (first)
+ {
+ case 10:
+ case 192 when second == 168:
+ case 172 when second is >= 16 and <= 31:
+ ThrowLocalAddressException(ip);
+ break;
+ }
+
+ // IPv6
+ if (IPAddress.IsLoopback(ip) ||
+ ip.IsIPv6LinkLocal ||
+ ip.IsIPv6SiteLocal ||
+ ip.IsIPv6UniqueLocal)
+ {
+ ThrowLocalAddressException(ip);
+ }
+ }
+
+ private void ThrowLocalAddressException(IPAddress ip)
+ {
+ throw new InvalidAddressException($"{ip.ToString()} is a local address");
+ }
+}
+
+public sealed class InvalidAddressException : ArgumentException
+{
+ internal InvalidAddressException()
+ {
+ }
+
+ [Obsolete("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.")]
+ internal InvalidAddressException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+
+ internal InvalidAddressException(string? message) : base(message)
+ {
+ }
+
+ internal InvalidAddressException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+}
diff --git a/Robust.Shared/Network/IHttpManager.cs b/Robust.Shared/Network/IHttpManager.cs
new file mode 100644
index 00000000000..e8c249faeee
--- /dev/null
+++ b/Robust.Shared/Network/IHttpManager.cs
@@ -0,0 +1,15 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Robust.Shared.Network;
+
+public interface IHttpManager
+{
+ Task GetStreamAsync(Uri uri, CancellationToken cancel);
+
+ Task GetStringAsync(Uri uri, CancellationToken cancel);
+
+ Task GetFromJsonAsync(Uri uri, CancellationToken cancel = default);
+}
diff --git a/Robust.Shared/Network/IHttpManagerInternal.cs b/Robust.Shared/Network/IHttpManagerInternal.cs
new file mode 100644
index 00000000000..9bb9e2c70a4
--- /dev/null
+++ b/Robust.Shared/Network/IHttpManagerInternal.cs
@@ -0,0 +1,6 @@
+namespace Robust.Shared.Network;
+
+public interface IHttpManagerInternal : IHttpManager
+{
+ void Shutdown();
+}
diff --git a/Robust.Shared/SharedIoC.cs b/Robust.Shared/SharedIoC.cs
index d743f5bda4b..581b4f9bedf 100644
--- a/Robust.Shared/SharedIoC.cs
+++ b/Robust.Shared/SharedIoC.cs
@@ -1,13 +1,10 @@
using Robust.Shared.Asynchronous;
-using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
-using Robust.Shared.Localization;
using Robust.Shared.Log;
-using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision;
@@ -50,6 +47,8 @@ public static void RegisterIoC(IDependencyCollection deps)
deps.Register();
deps.Register();
deps.Register();
+ deps.Register();
+ deps.Register();
}
}
}