From 5a2b88dc3e3ddd33d0ebfd5646834d1d48ba300c Mon Sep 17 00:00:00 2001 From: Vasilis The Pikachu Date: Mon, 29 Sep 2025 19:03:10 +0200 Subject: [PATCH 1/7] Code --- .../Data/TrackedCommunity.cs | 17 ++++--- .../Controllers/ServerListController.cs | 31 ++++++++++-- SS14.ServerHub/HubOptions.cs | 11 +++-- .../Pages/Servers/Communities/View.cshtml.cs | 49 +++++++++++-------- 4 files changed, 74 insertions(+), 34 deletions(-) diff --git a/SS14.ServerHub.Shared/Data/TrackedCommunity.cs b/SS14.ServerHub.Shared/Data/TrackedCommunity.cs index 369ad43..3c29d53 100644 --- a/SS14.ServerHub.Shared/Data/TrackedCommunity.cs +++ b/SS14.ServerHub.Shared/Data/TrackedCommunity.cs @@ -9,34 +9,39 @@ public sealed class TrackedCommunity /// ID of this entity in the database. /// public int Id { get; set; } - + /// /// The name of this community, as displayed throughout the UI to admins. /// public string Name { get; set; } = default!; - + /// /// Any useful notes admins may want to store about this community. /// public string Notes { get; set; } = default!; - + /// /// The time this community was created by an admin. /// public DateTime Created { get; set; } - + /// /// The last time any information for this community was updated by an admin. /// public DateTime LastUpdated { get; set; } - + /// /// This community is banned, and server advertisements should be disallowed. /// public bool IsBanned { get; set; } + /// + /// This community is except from only advertising a limited amount of servers from one IP address + /// + public bool IsExceptFromMaxAdvertisements { get; set; } + // Navigation properties public List Addresses { get; set; } = default!; public List Domains { get; set; } = default!; public List InfoMatches { get; set; } = default!; -} \ No newline at end of file +} diff --git a/SS14.ServerHub/Controllers/ServerListController.cs b/SS14.ServerHub/Controllers/ServerListController.cs index a2cf1c4..15a8447 100644 --- a/SS14.ServerHub/Controllers/ServerListController.cs +++ b/SS14.ServerHub/Controllers/ServerListController.cs @@ -53,7 +53,7 @@ public async Task> Get() return dbInfos; } - + [EnableCors(CorsPolicies.PolicyHubPublic)] [HttpGet("info")] public async Task GetServerInfo(string url) @@ -65,7 +65,7 @@ public async Task GetServerInfo(string url) if (dbInfo == null) return NotFound(); - + return Ok((RawJson?) dbInfo.InfoData); } @@ -118,6 +118,18 @@ parsedAddress.Scheme is not (Ss14UriHelper.SchemeSs14 or Ss14UriHelper.SchemeSs1 return Unauthorized("Your server has been blocked from advertising on the hub. If you believe this to be in error, please contact us."); } + if (senderIp != null) + { + // Check the current number of advertised servers from this IP. + var amount = await _dbContext.AdvertisedServer + .Where(s => s.AdvertiserAddress == senderIp) + .Where(s => s.Expires > DateTime.UtcNow) + .CountAsync(); + + if (amount >= HubOptions.MaxServersPerIp && !await CheckExceptFromMaxAdvertisements(parsedAddress)) + return Forbid($"You cannot advertise more then {amount} servers from one IP address, please contact us if you require an increase."); + } + var inferredTags = InferTags(statusJson); // Check if a server with this address already exists. @@ -149,7 +161,7 @@ parsedAddress.Scheme is not (Ss14UriHelper.SchemeSs14 or Ss14UriHelper.SchemeSs1 StatusData = statusJson, InferredTags = inferredTags }); - + await _dbContext.SaveChangesAsync(); return NoContent(); } @@ -176,11 +188,11 @@ parsedAddress.Scheme is not (Ss14UriHelper.SchemeSs14 or Ss14UriHelper.SchemeSs1 { return (UnprocessableEntity($"/status response data was too large (max: {maxStatusSize} KiB)"), null, null); } - + var statusData = JsonSerializer.Deserialize(statusResponse); if (statusData == null) throw new InvalidDataException("Status cannot be null"); - + if (string.IsNullOrWhiteSpace(statusData.Name)) return (UnprocessableEntity("Server name cannot be empty"), null, null); @@ -270,6 +282,15 @@ private BanCheckResult CheckMatchedCommunitiesForBan(Uri address, List b.TrackedCommunity.IsBanned); } + private async Task CheckExceptFromMaxAdvertisements(Uri advertisementUri) + { + var matched = new List(); + + await CommunityMatcher.MatchCommunities(_dbContext, advertisementUri, matched, CancellationToken.None); + + return matched.FirstOrDefault(x => x.IsExceptFromMaxAdvertisements) != null; + } + private static string[] InferTags(byte[] statusDataJson) { var statusData = JsonSerializer.Deserialize(statusDataJson)!; diff --git a/SS14.ServerHub/HubOptions.cs b/SS14.ServerHub/HubOptions.cs index 6cbf09d..15bfd62 100644 --- a/SS14.ServerHub/HubOptions.cs +++ b/SS14.ServerHub/HubOptions.cs @@ -3,7 +3,7 @@ public sealed class HubOptions { public const string Position = "Hub"; - + public float AdvertisementExpireMinutes { get; set; } = 3; /// @@ -22,9 +22,14 @@ public sealed class HubOptions /// When fetching /status from advertised servers, maximum size of response bodies in kilobytes. /// public int MaxStatusResponseSize = 2; - + /// /// When fetching /info from advertised servers, maximum size of response bodies in kilobytes. /// public int MaxInfoResponseSize = 10; -} \ No newline at end of file + + /// + /// What is the maximum number of servers one ip address can advertise? + /// + public const int MaxServersPerIp = 2; +} diff --git a/SS14.Web/Areas/Admin/Pages/Servers/Communities/View.cshtml.cs b/SS14.Web/Areas/Admin/Pages/Servers/Communities/View.cshtml.cs index 4e66855..8049343 100644 --- a/SS14.Web/Areas/Admin/Pages/Servers/Communities/View.cshtml.cs +++ b/SS14.Web/Areas/Admin/Pages/Servers/Communities/View.cshtml.cs @@ -29,7 +29,7 @@ public sealed class AddAddressModel { public string Address { get; set; } } - + public sealed class AddDomainModel { public string Domain { get; set; } @@ -46,14 +46,15 @@ public sealed class InputModel public string Name { get; set; } public string Notes { get; set; } public bool IsBanned { get; set; } + public bool IsExceptFromMaxAdvertisements { get; set; } } - + public View(HubDbContext dbContext, HubAuditLogManager hubAuditLog) { _dbContext = dbContext; _hubAuditLog = hubAuditLog; } - + public async Task OnGetAsync(int id) { Community = await _dbContext.TrackedCommunity @@ -62,7 +63,7 @@ public async Task OnGetAsync(int id) .Include(c => c.InfoMatches) .AsSplitQuery() .SingleOrDefaultAsync(u => u.Id == id); - + if (Community == null) return NotFound("Community not found"); @@ -70,9 +71,10 @@ public async Task OnGetAsync(int id) { Name = Community.Name, IsBanned = Community.IsBanned, + IsExceptFromMaxAdvertisements = Community.IsExceptFromMaxAdvertisements, Notes = Community.Notes }; - + return Page(); } @@ -86,7 +88,7 @@ public async Task OnPostSaveAsync(int id) var inputNotes = Input.Notes ?? ""; var anyChange = false; - + if (Community.Name != inputName) { _hubAuditLog.Log(User, new HubAuditCommunityChangedName(Community, Community.Name, inputName)); @@ -108,6 +110,13 @@ public async Task OnPostSaveAsync(int id) anyChange = true; } + if (Community.IsExceptFromMaxAdvertisements != Input.IsExceptFromMaxAdvertisements) + { + _hubAuditLog.Log(User, new HubAuditCommunityChangedBanned(Community, Community.IsExceptFromMaxAdvertisements, Input.IsExceptFromMaxAdvertisements)); + Community.IsExceptFromMaxAdvertisements = Input.IsExceptFromMaxAdvertisements; + anyChange = true; + } + if (!anyChange) return RedirectToPage(new { id = Community.Id }); @@ -119,11 +128,11 @@ public async Task OnPostSaveAsync(int id) return RedirectToPage(new { id = Community.Id }); } - + public async Task OnPostAddAddressAsync(int id) { await using var tx = await _dbContext.Database.BeginTransactionAsync(); - + Community = await _dbContext.TrackedCommunity.SingleOrDefaultAsync(c => c.Id == id); if (Community == null) return NotFound("Community not found"); @@ -138,29 +147,29 @@ public async Task OnPostAddAddressAsync(int id) { Address = cidr, TrackedCommunityId = id }; - + _dbContext.TrackedCommunityAddress.Add(address); Community.LastUpdated = DateTime.UtcNow; await _dbContext.SaveChangesAsync(); _hubAuditLog.Log(User, new HubAuditCommunityAddressAdd(Community, address)); - + await _dbContext.SaveChangesAsync(); await tx.CommitAsync(); - + StatusMessage = "Address added"; return RedirectToPage(new { id = Community.Id }); } - + public async Task OnPostDeleteAddressAsync(int address) { var addressEnt = await _dbContext.TrackedCommunityAddress .Include(c => c.TrackedCommunity) .SingleOrDefaultAsync(c => c.Id == address); - + if (addressEnt == null) return NotFound("Address not found"); @@ -174,7 +183,7 @@ public async Task OnPostDeleteAddressAsync(int address) return RedirectToPage(new { id = addressEnt.TrackedCommunityId }); } - + public async Task OnPostAddDomainAsync(int id) { await using var tx = await _dbContext.Database.BeginTransactionAsync(); @@ -200,22 +209,22 @@ public async Task OnPostAddDomainAsync(int id) await _dbContext.SaveChangesAsync(); _hubAuditLog.Log(User, new HubAuditCommunityDomainAdd(Community, domainEnt)); - + await _dbContext.SaveChangesAsync(); - + await tx.CommitAsync(); - + StatusMessage = "Domain added"; return RedirectToPage(new { id = Community.Id }); } - + public async Task OnPostDeleteDomainAsync(int domain) { var domainEnt = await _dbContext.TrackedCommunityDomain .Include(c => c.TrackedCommunity) .SingleOrDefaultAsync(c => c.Id == domain); - + if (domainEnt == null) return NotFound("Domain not found"); @@ -319,4 +328,4 @@ private async Task InsertInfoMatch( return await _dbContext.TrackedCommunityInfoMatch.SingleAsync(x => x.Id == result); } -} \ No newline at end of file +} From 7006804abb650a239b2e72b31d516409013ff626 Mon Sep 17 00:00:00 2001 From: Vasilis The Pikachu Date: Mon, 29 Sep 2025 19:51:33 +0200 Subject: [PATCH 2/7] DB --- .../20250929170512_MaxAdvertIP.Designer.cs | 311 ++++++++++++++++++ .../Migrations/20250929170512_MaxAdvertIP.cs | 26 ++ .../Migrations/HubDbContextModelSnapshot.cs | 3 + 3 files changed, 340 insertions(+) create mode 100644 SS14.ServerHub.Shared/Migrations/20250929170512_MaxAdvertIP.Designer.cs create mode 100644 SS14.ServerHub.Shared/Migrations/20250929170512_MaxAdvertIP.cs diff --git a/SS14.ServerHub.Shared/Migrations/20250929170512_MaxAdvertIP.Designer.cs b/SS14.ServerHub.Shared/Migrations/20250929170512_MaxAdvertIP.Designer.cs new file mode 100644 index 0000000..897f44e --- /dev/null +++ b/SS14.ServerHub.Shared/Migrations/20250929170512_MaxAdvertIP.Designer.cs @@ -0,0 +1,311 @@ +// +using System; +using System.Net; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SS14.ServerHub.Shared.Data; + +#nullable disable + +namespace SS14.ServerHub.Shared.Migrations +{ + [DbContext(typeof(HubDbContext))] + [Migration("20250929170512_MaxAdvertIP")] + partial class MaxAdvertIP + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SS14.ServerHub.Shared.Data.AdvertisedServer", b => + { + b.Property("AdvertisedServerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("AdvertisedServerId")); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("AdvertiserAddress") + .HasColumnType("inet"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b.Property("InferredTags") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("InfoData") + .HasColumnType("jsonb"); + + b.Property("StatusData") + .HasColumnType("jsonb"); + + b.HasKey("AdvertisedServerId"); + + b.HasIndex("Address") + .IsUnique(); + + b.ToTable("AdvertisedServer"); + + b.HasCheckConstraint("AddressSs14Uri", "\"Address\" LIKE 'ss14://%' OR \"Address\" LIKE 'ss14s://%'"); + }); + + modelBuilder.Entity("SS14.ServerHub.Shared.Data.HubAudit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Actor") + .HasColumnType("uuid"); + + b.Property("Data") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Time"); + + b.ToTable("HubAudit"); + }); + + modelBuilder.Entity("SS14.ServerHub.Shared.Data.ServerStatusArchive", b => + { + b.Property("AdvertisedServerId") + .HasColumnType("integer"); + + b.Property("ServerStatusArchiveId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ServerStatusArchiveId")); + + b.Property("AdvertiserAddress") + .HasColumnType("inet"); + + b.Property("InferredTags") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("StatusData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.HasKey("AdvertisedServerId", "ServerStatusArchiveId"); + + b.ToTable("ServerStatusArchive"); + }); + + modelBuilder.Entity("SS14.ServerHub.Shared.Data.TrackedCommunity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBanned") + .HasColumnType("boolean"); + + b.Property("IsExceptFromMaxAdvertisements") + .HasColumnType("boolean"); + + b.Property("LastUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("TrackedCommunity"); + }); + + modelBuilder.Entity("SS14.ServerHub.Shared.Data.TrackedCommunityAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("Address") + .HasColumnType("inet"); + + b.Property("TrackedCommunityId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TrackedCommunityId"); + + b.ToTable("TrackedCommunityAddress"); + }); + + modelBuilder.Entity("SS14.ServerHub.Shared.Data.TrackedCommunityDomain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DomainName") + .IsRequired() + .HasColumnType("text"); + + b.Property("TrackedCommunityId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TrackedCommunityId"); + + b.ToTable("TrackedCommunityDomain"); + }); + + modelBuilder.Entity("SS14.ServerHub.Shared.Data.TrackedCommunityInfoMatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Field") + .HasColumnType("integer"); + + b.Property("Path") + .IsRequired() + .HasColumnType("jsonpath"); + + b.Property("TrackedCommunityId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TrackedCommunityId"); + + b.ToTable("TrackedCommunityInfoMatch"); + }); + + modelBuilder.Entity("SS14.ServerHub.Shared.Data.UniqueServerName", b => + { + b.Property("AdvertisedServerId") + .HasColumnType("integer"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("FirstSeen") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSeen") + .HasColumnType("timestamp with time zone"); + + b.HasKey("AdvertisedServerId", "Name"); + + b.ToTable("UniqueServerName"); + }); + + modelBuilder.Entity("SS14.ServerHub.Shared.Data.ServerStatusArchive", b => + { + b.HasOne("SS14.ServerHub.Shared.Data.AdvertisedServer", "AdvertisedServer") + .WithMany() + .HasForeignKey("AdvertisedServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AdvertisedServer"); + }); + + modelBuilder.Entity("SS14.ServerHub.Shared.Data.TrackedCommunityAddress", b => + { + b.HasOne("SS14.ServerHub.Shared.Data.TrackedCommunity", "TrackedCommunity") + .WithMany("Addresses") + .HasForeignKey("TrackedCommunityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TrackedCommunity"); + }); + + modelBuilder.Entity("SS14.ServerHub.Shared.Data.TrackedCommunityDomain", b => + { + b.HasOne("SS14.ServerHub.Shared.Data.TrackedCommunity", "TrackedCommunity") + .WithMany("Domains") + .HasForeignKey("TrackedCommunityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TrackedCommunity"); + }); + + modelBuilder.Entity("SS14.ServerHub.Shared.Data.TrackedCommunityInfoMatch", b => + { + b.HasOne("SS14.ServerHub.Shared.Data.TrackedCommunity", "TrackedCommunity") + .WithMany("InfoMatches") + .HasForeignKey("TrackedCommunityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TrackedCommunity"); + }); + + modelBuilder.Entity("SS14.ServerHub.Shared.Data.UniqueServerName", b => + { + b.HasOne("SS14.ServerHub.Shared.Data.AdvertisedServer", "AdvertisedServer") + .WithMany() + .HasForeignKey("AdvertisedServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AdvertisedServer"); + }); + + modelBuilder.Entity("SS14.ServerHub.Shared.Data.TrackedCommunity", b => + { + b.Navigation("Addresses"); + + b.Navigation("Domains"); + + b.Navigation("InfoMatches"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SS14.ServerHub.Shared/Migrations/20250929170512_MaxAdvertIP.cs b/SS14.ServerHub.Shared/Migrations/20250929170512_MaxAdvertIP.cs new file mode 100644 index 0000000..4b6accc --- /dev/null +++ b/SS14.ServerHub.Shared/Migrations/20250929170512_MaxAdvertIP.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SS14.ServerHub.Shared.Migrations +{ + public partial class MaxAdvertIP : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsExceptFromMaxAdvertisements", + table: "TrackedCommunity", + type: "boolean", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsExceptFromMaxAdvertisements", + table: "TrackedCommunity"); + } + } +} diff --git a/SS14.ServerHub.Shared/Migrations/HubDbContextModelSnapshot.cs b/SS14.ServerHub.Shared/Migrations/HubDbContextModelSnapshot.cs index 0afba49..215ecaa 100644 --- a/SS14.ServerHub.Shared/Migrations/HubDbContextModelSnapshot.cs +++ b/SS14.ServerHub.Shared/Migrations/HubDbContextModelSnapshot.cs @@ -134,6 +134,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsBanned") .HasColumnType("boolean"); + b.Property("IsExceptFromMaxAdvertisements") + .HasColumnType("boolean"); + b.Property("LastUpdated") .HasColumnType("timestamp with time zone"); From 1d17d307c3bebd6ca45e1e561eb06ec6b20fd2f1 Mon Sep 17 00:00:00 2001 From: Vasilis The Pikachu Date: Mon, 29 Sep 2025 21:13:06 +0200 Subject: [PATCH 3/7] Oop --- .../Pages/Servers/Communities/View.cshtml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/SS14.Web/Areas/Admin/Pages/Servers/Communities/View.cshtml b/SS14.Web/Areas/Admin/Pages/Servers/Communities/View.cshtml index acb0951..ce4836e 100644 --- a/SS14.Web/Areas/Admin/Pages/Servers/Communities/View.cshtml +++ b/SS14.Web/Areas/Admin/Pages/Servers/Communities/View.cshtml @@ -38,8 +38,17 @@ +
+
+
+ + +
+
+
+ - +
Addresses
@@ -59,11 +68,11 @@
- + } @@ -92,11 +101,11 @@
- + } From 48c93a74e27f2a8c05b670c13b224ffaef33d38b Mon Sep 17 00:00:00 2001 From: Vasilis The Pikachu Date: Mon, 29 Sep 2025 22:04:04 +0200 Subject: [PATCH 4/7] Done --- .../Controllers/ServerListController.cs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/SS14.ServerHub/Controllers/ServerListController.cs b/SS14.ServerHub/Controllers/ServerListController.cs index 15a8447..bf34c90 100644 --- a/SS14.ServerHub/Controllers/ServerListController.cs +++ b/SS14.ServerHub/Controllers/ServerListController.cs @@ -118,23 +118,29 @@ parsedAddress.Scheme is not (Ss14UriHelper.SchemeSs14 or Ss14UriHelper.SchemeSs1 return Unauthorized("Your server has been blocked from advertising on the hub. If you believe this to be in error, please contact us."); } + var inferredTags = InferTags(statusJson); + + // Check if a server with this address already exists. + var addressEntity = + await _dbContext.AdvertisedServer.SingleOrDefaultAsync(a => a.Address == advertise.Address); + if (senderIp != null) { // Check the current number of advertised servers from this IP. - var amount = await _dbContext.AdvertisedServer + var adverts = await _dbContext.AdvertisedServer .Where(s => s.AdvertiserAddress == senderIp) .Where(s => s.Expires > DateTime.UtcNow) - .CountAsync(); + .ToListAsync(); - if (amount >= HubOptions.MaxServersPerIp && !await CheckExceptFromMaxAdvertisements(parsedAddress)) - return Forbid($"You cannot advertise more then {amount} servers from one IP address, please contact us if you require an increase."); - } + var isServerRenewingAdvertisement = addressEntity != null && addressEntity.Expires > DateTime.UtcNow; - var inferredTags = InferTags(statusJson); - - // Check if a server with this address already exists. - var addressEntity = - await _dbContext.AdvertisedServer.SingleOrDefaultAsync(a => a.Address == advertise.Address); + if (!isServerRenewingAdvertisement + && adverts.Count >= HubOptions.MaxServersPerIp + && !await CheckExceptFromMaxAdvertisements(parsedAddress)) + { + return Unauthorized($"You cannot advertise more then {HubOptions.MaxServersPerIp} servers from one IP address, please contact us if you require an increase."); + } + } var timeNow = DateTime.UtcNow; var newExpireTime = timeNow + TimeSpan.FromMinutes(options.AdvertisementExpireMinutes); From 6d31d9ef69d04c7e9b1e34a4ecb11733d5b84a85 Mon Sep 17 00:00:00 2001 From: Vasilis The Pikachu Date: Mon, 29 Sep 2025 23:53:04 +0200 Subject: [PATCH 5/7] Skill issue fix --- SS14.ServerHub.Shared/Data/TrackedCommunity.cs | 2 +- ...igner.cs => 20250929215222_MaxAdvertIP.Designer.cs} | 4 ++-- ...12_MaxAdvertIP.cs => 20250929215222_MaxAdvertIP.cs} | 4 ++-- .../Migrations/HubDbContextModelSnapshot.cs | 2 +- SS14.ServerHub/Controllers/ServerListController.cs | 6 +++--- .../Areas/Admin/Pages/Servers/Communities/View.cshtml | 4 ++-- .../Admin/Pages/Servers/Communities/View.cshtml.cs | 10 +++++----- 7 files changed, 16 insertions(+), 16 deletions(-) rename SS14.ServerHub.Shared/Migrations/{20250929170512_MaxAdvertIP.Designer.cs => 20250929215222_MaxAdvertIP.Designer.cs} (99%) rename SS14.ServerHub.Shared/Migrations/{20250929170512_MaxAdvertIP.cs => 20250929215222_MaxAdvertIP.cs} (85%) diff --git a/SS14.ServerHub.Shared/Data/TrackedCommunity.cs b/SS14.ServerHub.Shared/Data/TrackedCommunity.cs index 3c29d53..e098f4f 100644 --- a/SS14.ServerHub.Shared/Data/TrackedCommunity.cs +++ b/SS14.ServerHub.Shared/Data/TrackedCommunity.cs @@ -38,7 +38,7 @@ public sealed class TrackedCommunity /// /// This community is except from only advertising a limited amount of servers from one IP address /// - public bool IsExceptFromMaxAdvertisements { get; set; } + public bool IsExemptFromMaxAdvertisements { get; set; } // Navigation properties public List Addresses { get; set; } = default!; diff --git a/SS14.ServerHub.Shared/Migrations/20250929170512_MaxAdvertIP.Designer.cs b/SS14.ServerHub.Shared/Migrations/20250929215222_MaxAdvertIP.Designer.cs similarity index 99% rename from SS14.ServerHub.Shared/Migrations/20250929170512_MaxAdvertIP.Designer.cs rename to SS14.ServerHub.Shared/Migrations/20250929215222_MaxAdvertIP.Designer.cs index 897f44e..13be317 100644 --- a/SS14.ServerHub.Shared/Migrations/20250929170512_MaxAdvertIP.Designer.cs +++ b/SS14.ServerHub.Shared/Migrations/20250929215222_MaxAdvertIP.Designer.cs @@ -14,7 +14,7 @@ namespace SS14.ServerHub.Shared.Migrations { [DbContext(typeof(HubDbContext))] - [Migration("20250929170512_MaxAdvertIP")] + [Migration("20250929215222_MaxAdvertIP")] partial class MaxAdvertIP { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -136,7 +136,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("IsBanned") .HasColumnType("boolean"); - b.Property("IsExceptFromMaxAdvertisements") + b.Property("IsExemptFromMaxAdvertisements") .HasColumnType("boolean"); b.Property("LastUpdated") diff --git a/SS14.ServerHub.Shared/Migrations/20250929170512_MaxAdvertIP.cs b/SS14.ServerHub.Shared/Migrations/20250929215222_MaxAdvertIP.cs similarity index 85% rename from SS14.ServerHub.Shared/Migrations/20250929170512_MaxAdvertIP.cs rename to SS14.ServerHub.Shared/Migrations/20250929215222_MaxAdvertIP.cs index 4b6accc..f0f80ac 100644 --- a/SS14.ServerHub.Shared/Migrations/20250929170512_MaxAdvertIP.cs +++ b/SS14.ServerHub.Shared/Migrations/20250929215222_MaxAdvertIP.cs @@ -9,7 +9,7 @@ public partial class MaxAdvertIP : Migration protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn( - name: "IsExceptFromMaxAdvertisements", + name: "IsExemptFromMaxAdvertisements", table: "TrackedCommunity", type: "boolean", nullable: false, @@ -19,7 +19,7 @@ protected override void Up(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropColumn( - name: "IsExceptFromMaxAdvertisements", + name: "IsExemptFromMaxAdvertisements", table: "TrackedCommunity"); } } diff --git a/SS14.ServerHub.Shared/Migrations/HubDbContextModelSnapshot.cs b/SS14.ServerHub.Shared/Migrations/HubDbContextModelSnapshot.cs index 215ecaa..7239126 100644 --- a/SS14.ServerHub.Shared/Migrations/HubDbContextModelSnapshot.cs +++ b/SS14.ServerHub.Shared/Migrations/HubDbContextModelSnapshot.cs @@ -134,7 +134,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsBanned") .HasColumnType("boolean"); - b.Property("IsExceptFromMaxAdvertisements") + b.Property("IsExemptFromMaxAdvertisements") .HasColumnType("boolean"); b.Property("LastUpdated") diff --git a/SS14.ServerHub/Controllers/ServerListController.cs b/SS14.ServerHub/Controllers/ServerListController.cs index bf34c90..38c47ba 100644 --- a/SS14.ServerHub/Controllers/ServerListController.cs +++ b/SS14.ServerHub/Controllers/ServerListController.cs @@ -136,7 +136,7 @@ parsedAddress.Scheme is not (Ss14UriHelper.SchemeSs14 or Ss14UriHelper.SchemeSs1 if (!isServerRenewingAdvertisement && adverts.Count >= HubOptions.MaxServersPerIp - && !await CheckExceptFromMaxAdvertisements(parsedAddress)) + && !await CheckExemptFromMaxAdvertisements(parsedAddress)) { return Unauthorized($"You cannot advertise more then {HubOptions.MaxServersPerIp} servers from one IP address, please contact us if you require an increase."); } @@ -288,13 +288,13 @@ private BanCheckResult CheckMatchedCommunitiesForBan(Uri address, List b.TrackedCommunity.IsBanned); } - private async Task CheckExceptFromMaxAdvertisements(Uri advertisementUri) + private async Task CheckExemptFromMaxAdvertisements(Uri advertisementUri) { var matched = new List(); await CommunityMatcher.MatchCommunities(_dbContext, advertisementUri, matched, CancellationToken.None); - return matched.FirstOrDefault(x => x.IsExceptFromMaxAdvertisements) != null; + return matched.FirstOrDefault(x => x.IsExemptFromMaxAdvertisements) != null; } private static string[] InferTags(byte[] statusDataJson) diff --git a/SS14.Web/Areas/Admin/Pages/Servers/Communities/View.cshtml b/SS14.Web/Areas/Admin/Pages/Servers/Communities/View.cshtml index ce4836e..fac9b1e 100644 --- a/SS14.Web/Areas/Admin/Pages/Servers/Communities/View.cshtml +++ b/SS14.Web/Areas/Admin/Pages/Servers/Communities/View.cshtml @@ -41,8 +41,8 @@
- - + +
diff --git a/SS14.Web/Areas/Admin/Pages/Servers/Communities/View.cshtml.cs b/SS14.Web/Areas/Admin/Pages/Servers/Communities/View.cshtml.cs index 8049343..8a9ee76 100644 --- a/SS14.Web/Areas/Admin/Pages/Servers/Communities/View.cshtml.cs +++ b/SS14.Web/Areas/Admin/Pages/Servers/Communities/View.cshtml.cs @@ -46,7 +46,7 @@ public sealed class InputModel public string Name { get; set; } public string Notes { get; set; } public bool IsBanned { get; set; } - public bool IsExceptFromMaxAdvertisements { get; set; } + public bool IsExemptFromMaxAdvertisements { get; set; } } public View(HubDbContext dbContext, HubAuditLogManager hubAuditLog) @@ -71,7 +71,7 @@ public async Task OnGetAsync(int id) { Name = Community.Name, IsBanned = Community.IsBanned, - IsExceptFromMaxAdvertisements = Community.IsExceptFromMaxAdvertisements, + IsExemptFromMaxAdvertisements = Community.IsExemptFromMaxAdvertisements, Notes = Community.Notes }; @@ -110,10 +110,10 @@ public async Task OnPostSaveAsync(int id) anyChange = true; } - if (Community.IsExceptFromMaxAdvertisements != Input.IsExceptFromMaxAdvertisements) + if (Community.IsExemptFromMaxAdvertisements != Input.IsExemptFromMaxAdvertisements) { - _hubAuditLog.Log(User, new HubAuditCommunityChangedBanned(Community, Community.IsExceptFromMaxAdvertisements, Input.IsExceptFromMaxAdvertisements)); - Community.IsExceptFromMaxAdvertisements = Input.IsExceptFromMaxAdvertisements; + _hubAuditLog.Log(User, new HubAuditCommunityChangedBanned(Community, Community.IsExemptFromMaxAdvertisements, Input.IsExemptFromMaxAdvertisements)); + Community.IsExemptFromMaxAdvertisements = Input.IsExemptFromMaxAdvertisements; anyChange = true; } From 2e4240e047c531da6281b0e087ea6cc527a6747a Mon Sep 17 00:00:00 2001 From: Vasilis The Pikachu Date: Mon, 29 Sep 2025 23:57:21 +0200 Subject: [PATCH 6/7] This can stay as a count actually --- SS14.ServerHub/Controllers/ServerListController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SS14.ServerHub/Controllers/ServerListController.cs b/SS14.ServerHub/Controllers/ServerListController.cs index 38c47ba..c22e988 100644 --- a/SS14.ServerHub/Controllers/ServerListController.cs +++ b/SS14.ServerHub/Controllers/ServerListController.cs @@ -127,15 +127,15 @@ parsedAddress.Scheme is not (Ss14UriHelper.SchemeSs14 or Ss14UriHelper.SchemeSs1 if (senderIp != null) { // Check the current number of advertised servers from this IP. - var adverts = await _dbContext.AdvertisedServer + var count = await _dbContext.AdvertisedServer .Where(s => s.AdvertiserAddress == senderIp) .Where(s => s.Expires > DateTime.UtcNow) - .ToListAsync(); + .CountAsync(); var isServerRenewingAdvertisement = addressEntity != null && addressEntity.Expires > DateTime.UtcNow; if (!isServerRenewingAdvertisement - && adverts.Count >= HubOptions.MaxServersPerIp + && count >= HubOptions.MaxServersPerIp && !await CheckExemptFromMaxAdvertisements(parsedAddress)) { return Unauthorized($"You cannot advertise more then {HubOptions.MaxServersPerIp} servers from one IP address, please contact us if you require an increase."); From 74d61e95c3a6df20c13c26a51dec047e4a08eb7e Mon Sep 17 00:00:00 2001 From: Myra Date: Sat, 27 Dec 2025 17:55:29 +0100 Subject: [PATCH 7/7] Update SS14.ServerHub/Controllers/ServerListController.cs Co-authored-by: deathride58 --- SS14.ServerHub/Controllers/ServerListController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SS14.ServerHub/Controllers/ServerListController.cs b/SS14.ServerHub/Controllers/ServerListController.cs index c22e988..f80481b 100644 --- a/SS14.ServerHub/Controllers/ServerListController.cs +++ b/SS14.ServerHub/Controllers/ServerListController.cs @@ -138,7 +138,7 @@ parsedAddress.Scheme is not (Ss14UriHelper.SchemeSs14 or Ss14UriHelper.SchemeSs1 && count >= HubOptions.MaxServersPerIp && !await CheckExemptFromMaxAdvertisements(parsedAddress)) { - return Unauthorized($"You cannot advertise more then {HubOptions.MaxServersPerIp} servers from one IP address, please contact us if you require an increase."); + return Unauthorized($"You cannot advertise more than {HubOptions.MaxServersPerIp} servers from one IP address, please contact us if you require an increase."); } }