diff --git a/Giants.Services/Services/IServerRegistryService.cs b/Giants.Services/Services/IServerRegistryService.cs index 1db18e0..0022715 100644 --- a/Giants.Services/Services/IServerRegistryService.cs +++ b/Giants.Services/Services/IServerRegistryService.cs @@ -7,6 +7,8 @@ { Task DeleteServer(string ipAddress); + Task DeleteServer(string ipAddress, string gameName, int port); + Task> GetAllServers(); Task AddServer(ServerInfo server); diff --git a/Giants.Services/Services/ServerRegistryService.cs b/Giants.Services/Services/ServerRegistryService.cs index dcf13a5..b38962f 100644 --- a/Giants.Services/Services/ServerRegistryService.cs +++ b/Giants.Services/Services/ServerRegistryService.cs @@ -4,16 +4,18 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using AutoMapper; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; public class ServerRegistryService : IServerRegistryService { - private static readonly string[] SupportedGameNames = new[] { "Giants" }; + private static readonly string[] SupportedGameNames = new[] { "Giants", "Giants Beta" }; private readonly ILogger logger; private readonly IServerRegistryStore registryStore; private readonly IConfiguration configuration; private readonly int maxServerCount; + private readonly int maxServersPerIpGame; public ServerRegistryService( ILogger logger, @@ -24,6 +26,7 @@ this.registryStore = registryStore; this.configuration = configuration; this.maxServerCount = Convert.ToInt32(this.configuration["MaxServerCount"]); + this.maxServersPerIpGame = Convert.ToInt32(this.configuration["MaxServersPerIpGame"]); } public async Task AddServer( @@ -37,6 +40,12 @@ throw new ArgumentException($"Unsupported game name {serverInfo.GameName}", nameof(serverInfo)); } + var existingServers = await this.registryStore.GetServerInfos(whereExpression: x => x.HostIpAddress == serverInfo.HostIpAddress); + if (existingServers.GroupBy(g => g.GameName).Any(g => g.Count() > this.maxServersPerIpGame)) + { + throw new InvalidOperationException("Exceeded maximum servers per IP."); + } + await this.registryStore.UpsertServerInfo(serverInfo); } @@ -46,16 +55,36 @@ .Take(this.maxServerCount); } + // Old API, soon to be deprecated public async Task DeleteServer(string ipAddress) { ArgumentUtility.CheckStringForNullOrEmpty(ipAddress, nameof(ipAddress)); - ServerInfo serverInfo = await this.registryStore.GetServerInfo(ipAddress); + var serverInfos = await this.registryStore.GetServerInfos(whereExpression: x => x.HostIpAddress == ipAddress); - if (serverInfo != null) + foreach (var serverInfo in serverInfos) { await this.registryStore.DeleteServer(serverInfo.id); } } + + public async Task DeleteServer(string ipAddress, string gameName, int port) + { + ArgumentUtility.CheckStringForNullOrEmpty(ipAddress, nameof(ipAddress)); + ArgumentUtility.CheckStringForNullOrEmpty(gameName, nameof(gameName)); + ArgumentUtility.CheckForNonnegativeInt(port, nameof(port)); + + var existingServerInfo = (await this.registryStore.GetServerInfos( + whereExpression: + x => x.HostIpAddress == ipAddress && + x.Port == port && + x.GameName.Equals(gameName, StringComparison.OrdinalIgnoreCase))) + .FirstOrDefault(); + + if (existingServerInfo != null) + { + await this.registryStore.DeleteServer(existingServerInfo.id); + } + } } } diff --git a/Giants.Services/Store/CosmosDbServerRegistryStore.cs b/Giants.Services/Store/CosmosDbServerRegistryStore.cs index f16d4b1..ac8ba08 100644 --- a/Giants.Services/Store/CosmosDbServerRegistryStore.cs +++ b/Giants.Services/Store/CosmosDbServerRegistryStore.cs @@ -41,10 +41,11 @@ bool includeExpired = false, string partitionKey = null) { - ConcurrentDictionary serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache); + ConcurrentDictionary> serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache); IQueryable serverInfoQuery = serverInfo .Values + .SelectMany(s => s) .AsQueryable(); if (whereExpression != null) @@ -67,10 +68,11 @@ Expression> whereExpression = null, string partitionKey = null) { - ConcurrentDictionary serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache); + ConcurrentDictionary> serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache); IQueryable serverInfoQuery = serverInfo .Values + .SelectMany(s => s) .AsQueryable(); if (serverInfoQuery != null) @@ -88,42 +90,20 @@ .ToList(); } - public async Task GetServerInfo(string ipAddress) - { - ArgumentUtility.CheckStringForNullOrEmpty(ipAddress, nameof(ipAddress)); - - ConcurrentDictionary serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache); - if (serverInfo.ContainsKey(ipAddress)) - { - try - { - return serverInfo[ipAddress]; - } - catch (Exception e) - { - this.logger.LogInformation("Cached server for {IPAddress} no longer found: {Exception}", ipAddress, e.ToString()); - // May have been removed from the cache by another thread. Ignore and query DB instead. - } - } - - return await this.client.GetItemById(ipAddress); - } - public async Task UpsertServerInfo(ServerInfo serverInfo) { ArgumentUtility.CheckForNull(serverInfo, nameof(serverInfo)); // Check cache before we write to store - ConcurrentDictionary allServerInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache); + ConcurrentDictionary> allServerInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache); if (allServerInfo.ContainsKey(serverInfo.HostIpAddress)) { - ServerInfo existingServerInfo = allServerInfo[serverInfo.HostIpAddress]; + ServerInfo existingServerInfo = FindExistingServerForHostIp(allServerInfo[serverInfo.HostIpAddress], serverInfo); // DDOS protection: skip write to Cosmos if parameters have not changed, // or it's not been long enough. - if (existingServerInfo.Equals(serverInfo) - && Math.Abs((existingServerInfo.LastHeartbeat - serverInfo.LastHeartbeat).TotalMinutes) < ServerRefreshIntervalInMinutes) + if (existingServerInfo != null && Math.Abs((existingServerInfo.LastHeartbeat - serverInfo.LastHeartbeat).TotalMinutes) < ServerRefreshIntervalInMinutes) { this.logger.LogInformation("State for {IPAddress} is unchanged. Skipping write to store.", serverInfo.HostIpAddress); return; @@ -142,11 +122,11 @@ if (allServerInfo.ContainsKey(serverInfo.HostIpAddress)) { - allServerInfo[serverInfo.HostIpAddress] = serverInfo; + allServerInfo[serverInfo.HostIpAddress].Add(serverInfo); } else { - allServerInfo.TryAdd(serverInfo.HostIpAddress, serverInfo); + allServerInfo.TryAdd(serverInfo.HostIpAddress, new List() { serverInfo }); } } @@ -155,8 +135,21 @@ await this.client.DeleteItem(id, partitionKey); // Remove from cache - ConcurrentDictionary allServerInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache); - allServerInfo.TryRemove(id, out ServerInfo _); + ConcurrentDictionary> allServerInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache); + if (allServerInfo.ContainsKey(id)) + { + var serverInfoCopy = allServerInfo[id].Where(s => s.id != id).ToList(); + if (!serverInfoCopy.Any()) + { + // No remaining servers, remove the key + allServerInfo.TryRemove(id, out IList _); + } + else + { + // Shallow-copy and replace to keep thread safety guarantee + allServerInfo[id] = serverInfoCopy; + } + } } public async Task DeleteServers(IEnumerable ids, string partitionKey = null) @@ -182,15 +175,39 @@ await this.client.Initialize(); } - private async Task> PopulateCache(ICacheEntry entry) + private async Task>> PopulateCache(ICacheEntry entry) { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1); - IDictionary serverInfo = - (await this.client.GetItems()) - .ToDictionary(x => x.HostIpAddress, y => y); + var allServerInfo = (await this.client.GetItems()); + var serverInfoDictionary = new ConcurrentDictionary>(); - return new ConcurrentDictionary(serverInfo); + foreach (var serverInfo in allServerInfo) + { + if (!serverInfoDictionary.ContainsKey(serverInfo.HostIpAddress)) + { + serverInfoDictionary[serverInfo.HostIpAddress] = new List() { serverInfo }; + } + else + { + serverInfoDictionary[serverInfo.HostIpAddress].Add(serverInfo); + } + } + + return serverInfoDictionary; + } + + private static ServerInfo FindExistingServerForHostIp(IList serverInfoForHostIp, ServerInfo candidateServerInfo) + { + foreach (var existingServerInfo in serverInfoForHostIp) + { + if (existingServerInfo.Equals(candidateServerInfo)) + { + return existingServerInfo; + } + } + + return null; } } } \ No newline at end of file diff --git a/Giants.Services/Store/IServerRegistryStore.cs b/Giants.Services/Store/IServerRegistryStore.cs index 9fcf522..a2a699a 100644 --- a/Giants.Services/Store/IServerRegistryStore.cs +++ b/Giants.Services/Store/IServerRegistryStore.cs @@ -13,8 +13,6 @@ Task DeleteServers(IEnumerable ids, string partitionKey = null); - Task GetServerInfo(string ipAddress); - Task> GetServerInfos(Expression> whereExpression = null, bool includeExpired = false, string partitionKey = null); Task> GetServerInfos( diff --git a/Giants.WebApi/Controllers/CommunityController.cs b/Giants.WebApi/Controllers/CommunityController.cs index 35b3633..268f9fe 100644 --- a/Giants.WebApi/Controllers/CommunityController.cs +++ b/Giants.WebApi/Controllers/CommunityController.cs @@ -6,6 +6,7 @@ [ApiController] [ApiVersion("1.0")] + [ApiVersion("1.1")] [Route("api/[controller]")] public class CommunityController : ControllerBase { diff --git a/Giants.WebApi/Controllers/CrashReportsController.cs b/Giants.WebApi/Controllers/CrashReportsController.cs index b4e5724..b68148a 100644 --- a/Giants.WebApi/Controllers/CrashReportsController.cs +++ b/Giants.WebApi/Controllers/CrashReportsController.cs @@ -13,6 +13,7 @@ [ApiController] [ApiVersion("1.0")] + [ApiVersion("1.1")] [Route("api/[controller]")] public class CrashReportsController : ControllerBase { diff --git a/Giants.WebApi/Controllers/ServersController.cs b/Giants.WebApi/Controllers/ServersController.cs index 7bd54fe..44bac58 100644 --- a/Giants.WebApi/Controllers/ServersController.cs +++ b/Giants.WebApi/Controllers/ServersController.cs @@ -14,6 +14,7 @@ namespace Giants.Web.Controllers { [ApiController] [ApiVersion("1.0")] + [ApiVersion("1.1")] [Route("api/[controller]")] public class ServersController : ControllerBase { @@ -44,6 +45,17 @@ namespace Giants.Web.Controllers await this.serverRegistryService.DeleteServer(requestIpAddress); } + [HttpDelete] + [MapToApiVersion("1.1")] + public async Task DeleteServer(string gameName, int port) + { + string requestIpAddress = this.GetRequestIpAddress(); + + this.logger.LogInformation("Deleting server from {IPAddress}", requestIpAddress); + + await this.serverRegistryService.DeleteServer(requestIpAddress, gameName, port); + } + [HttpGet] public async Task> GetServers() { diff --git a/Giants.WebApi/Controllers/VersionController.cs b/Giants.WebApi/Controllers/VersionController.cs index 35635c5..8882a51 100644 --- a/Giants.WebApi/Controllers/VersionController.cs +++ b/Giants.WebApi/Controllers/VersionController.cs @@ -7,6 +7,7 @@ namespace Giants.WebApi.Controllers { [ApiController] [ApiVersion("1.0")] + [ApiVersion("1.1")] [Route("api/[controller]")] public class VersionController : ControllerBase { diff --git a/Giants.WebApi/appsettings.json b/Giants.WebApi/appsettings.json index b3e3b1e..fc6e249 100644 --- a/Giants.WebApi/appsettings.json +++ b/Giants.WebApi/appsettings.json @@ -12,6 +12,7 @@ "ServerTimeoutPeriodInMinutes": "7", "ServerCleanupIntervalInMinutes": "1", "MaxServerCount": 1000, + "MaxServersPerIpGame": 5, "DiscordUri": "https://discord.gg/Avj4azU", "BlobConnectionString": "", "CrashBlobContainerName": "crashes" diff --git a/Plugins/imp_gbs/imp_gbs.vcxproj b/Plugins/imp_gbs/imp_gbs.vcxproj index 5f694bd..371fef5 100644 --- a/Plugins/imp_gbs/imp_gbs.vcxproj +++ b/Plugins/imp_gbs/imp_gbs.vcxproj @@ -19,6 +19,7 @@ GiantsExp Win32Proj imp_gbs + 10.0 @@ -74,7 +75,7 @@ Level3 ProgramDatabase Default - stdcpplatest + stdcpp17 stdafx.h @@ -110,7 +111,7 @@ Level3 ProgramDatabase Default - stdcpplatest + stdcpp17 stdafx.h @@ -142,7 +143,7 @@ Use Level3 ProgramDatabase - stdcpplatest + stdcpp17 stdafx.h