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(); } } }