using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Content.Server.Chat.Managers; using Content.Server.Connection.IPIntel; using Content.Server.Database; using Content.Shared.CCVar; using Moq; using NUnit.Framework; using Robust.Shared.Configuration; using Robust.Shared.Log; using Robust.Shared.Timing; using Robust.UnitTesting; // ReSharper disable AccessToModifiedClosure namespace Content.Tests.Server.Connection; [TestFixture, TestOf(typeof(IPIntel))] [Parallelizable(ParallelScope.All)] public static class IPIntelTest { private static readonly IPAddress TestIp = IPAddress.Parse("192.0.2.1"); private static void CreateIPIntel( out IPIntel ipIntel, out IConfigurationManager cfg, Func apiResponse, Func realTime = null) { var dbManager = new Mock(); var gameTimingMock = new Mock(); gameTimingMock.SetupGet(gt => gt.RealTime) .Returns(realTime ?? (() => TimeSpan.Zero)); var logManager = new LogManager(); var gameTiming = gameTimingMock.Object; cfg = MockInterfaces.MakeConfigurationManager(gameTiming, logManager, loadCvarsFromTypes: [typeof(CCVars)]); ipIntel = new IPIntel( new FakeIPIntelApi(apiResponse), dbManager.Object, cfg, logManager, new Mock().Object, gameTiming ); } [Test] public static async Task TestSuccess() { CreateIPIntel( out var ipIntel, out _, RespondSuccess); var result = await ipIntel.QueryIPIntelRateLimited(TestIp); Assert.Multiple(() => { Assert.That(result.Score, Is.EqualTo(0.5f).Within(0.01f)); Assert.That(result.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success)); }); } [Test] public static async Task KnownRateLimitMinuteTest() { var source = RespondSuccess; var time = TimeSpan.Zero; CreateIPIntel( out var ipIntel, out var cfg, () => source(), () => time); cfg.SetCVar(CCVars.GameIPIntelMaxMinute, 9); for (var i = 0; i < 9; i++) { var result = await ipIntel.QueryIPIntelRateLimited(TestIp); Assert.That(result.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success)); } source = RespondTestFailed; var shouldBeRateLimited = await ipIntel.QueryIPIntelRateLimited(TestIp); Assert.That(shouldBeRateLimited.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited)); time += TimeSpan.FromMinutes(1.5); source = RespondSuccess; var shouldSucceed = await ipIntel.QueryIPIntelRateLimited(TestIp); Assert.That(shouldSucceed.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success)); } [Test] public static async Task KnownRateLimitMinuteTimingTest() { var source = RespondSuccess; var time = TimeSpan.Zero; CreateIPIntel( out var ipIntel, out var cfg, () => source(), () => time); cfg.SetCVar(CCVars.GameIPIntelMaxMinute, 1); // First query succeeds. var result = await ipIntel.QueryIPIntelRateLimited(TestIp); Assert.That(result.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success)); // Second is rate limited via known limit. source = RespondTestFailed; result = await ipIntel.QueryIPIntelRateLimited(TestIp); Assert.That(result.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited)); // Move 30 seconds into the future, should not be enough to unratelimit. time += TimeSpan.FromSeconds(30); var shouldBeRateLimited = await ipIntel.QueryIPIntelRateLimited(TestIp); Assert.That(shouldBeRateLimited.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited)); // Should be available again. source = RespondSuccess; time += TimeSpan.FromSeconds(35); var shouldSucceed = await ipIntel.QueryIPIntelRateLimited(TestIp); Assert.That(shouldSucceed.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success)); } [Test] public static async Task SuddenRateLimitTest() { var time = TimeSpan.Zero; var source = RespondRateLimited; CreateIPIntel( out var ipIntel, out _, () => source(), () => time); var test = await ipIntel.QueryIPIntelRateLimited(TestIp); Assert.That(test.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited)); source = RespondTestFailed; test = await ipIntel.QueryIPIntelRateLimited(TestIp); Assert.That(test.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited)); // King crimson idk I didn't watch JoJo past part 2. time += TimeSpan.FromMinutes(2); source = RespondSuccess; test = await ipIntel.QueryIPIntelRateLimited(TestIp); Assert.That(test.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Success)); } [Test] public static async Task SuddenRateLimitExponentialBackoffTest() { var time = TimeSpan.Zero; var source = RespondRateLimited; CreateIPIntel( out var ipIntel, out _, () => source(), () => time); IPIntel.IPIntelResult test; for (var i = 0; i < 5; i++) { time += TimeSpan.FromHours(1); test = await ipIntel.QueryIPIntelRateLimited(TestIp); Assert.That(test.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited)); } // After 5 sequential failed attempts, 1 minute should not be enough to get past the exponential backoff. time += TimeSpan.FromMinutes(1); source = RespondTestFailed; test = await ipIntel.QueryIPIntelRateLimited(TestIp); Assert.That(test.Code, Is.EqualTo(IPIntel.IPIntelResultCode.RateLimited)); } [Test] public static async Task ErrorTest() { CreateIPIntel( out var ipIntel, out _, RespondError); var resp = await ipIntel.QueryIPIntelRateLimited(TestIp); Assert.That(resp.Code, Is.EqualTo(IPIntel.IPIntelResultCode.Errored)); } [Test] [TestCase("0.0.0.0", ExpectedResult = true)] [TestCase("0.3.5.7", ExpectedResult = true)] [TestCase("127.0.0.1", ExpectedResult = true)] [TestCase("11.0.0.0", ExpectedResult = false)] [TestCase("10.0.1.0", ExpectedResult = true)] [TestCase("192.168.5.12", ExpectedResult = true)] [TestCase("192.167.0.1", ExpectedResult = false)] // Not an IPv4! [TestCase("::1", ExpectedResult = false)] public static bool TestIsReservedIpv4(string ipAddress) { return IPIntel.IsAddressReservedIpv4(IPAddress.Parse(ipAddress)); } [Test] // IPv4-mapped IPv6 should use IPv4 behavior. [TestCase("::ffff:0.0.0.0", ExpectedResult = true)] [TestCase("::ffff:0.3.5.7", ExpectedResult = true)] [TestCase("::ffff:127.0.0.1", ExpectedResult = true)] [TestCase("::ffff:11.0.0.0", ExpectedResult = false)] [TestCase("::ffff:10.0.1.0", ExpectedResult = true)] [TestCase("::ffff:192.168.5.12", ExpectedResult = true)] [TestCase("::ffff:192.167.0.1", ExpectedResult = false)] // Regular IPv6 tests. [TestCase("::1", ExpectedResult = true)] [TestCase("2001:db8::01", ExpectedResult = true)] [TestCase("2a01:4f8:252:4425::1234", ExpectedResult = false)] // Not an IPv6! [TestCase("127.0.0.1", ExpectedResult = false)] public static bool TestIsReservedIpv6(string ipAddress) { return IPIntel.IsAddressReservedIpv6(IPAddress.Parse(ipAddress)); } private static HttpResponseMessage RespondSuccess() { return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("0.5"), }; } private static HttpResponseMessage RespondRateLimited() { return new HttpResponseMessage(HttpStatusCode.TooManyRequests); } private static HttpResponseMessage RespondTestFailed() { throw new InvalidOperationException("API should not be queried at this part of the test."); } private static HttpResponseMessage RespondError() { return new HttpResponseMessage(HttpStatusCode.BadRequest) { Content = new StringContent("-4"), }; } } internal sealed class FakeIPIntelApi(Func response) : IIPIntelApi { public Task GetIPScore(IPAddress ip) { return Task.FromResult(response()); } }