mirror of
				https://github.com/CommonLoon102/NBloodServerSupervisor.git
				synced 2025-11-03 23:37:25 +01:00 
			
		
		
		
	add option to play custom maps on private servers (#9)
* add option to play custom maps on private servers * don't show every exception to the end-user * hash the IPs * add description about the purpose of the public custom map list * add punctuation to error messages
This commit is contained in:
		
							parent
							
								
									3417571b70
								
							
						
					
					
						commit
						3af4c9dfc7
					
				@ -9,6 +9,7 @@ namespace Common
 | 
			
		||||
    public static class CommandLineUtils
 | 
			
		||||
    {
 | 
			
		||||
        public static string BloodDir => Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "blood");
 | 
			
		||||
        public static string TempMapDir => Path.Combine(BloodDir, "tempmaps");
 | 
			
		||||
 | 
			
		||||
        public static string GetClientLaunchCommand(string host, int port, string modCommandLine) =>
 | 
			
		||||
            $"nblood -client {host} -port {port} {modCommandLine}";
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,8 @@ namespace Common
 | 
			
		||||
{
 | 
			
		||||
    public class Constants
 | 
			
		||||
    {
 | 
			
		||||
        public const long FileSizeLimit = 1024 * 1024;
 | 
			
		||||
 | 
			
		||||
        public static readonly IReadOnlyDictionary<string, Mod> SupportedMods = new Dictionary<string, Mod>()
 | 
			
		||||
        {
 | 
			
		||||
            { "BLOOD", new Mod("BLOOD", "Blood", "") },
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Net.NetworkInformation;
 | 
			
		||||
using System.IO;
 | 
			
		||||
 | 
			
		||||
namespace Common
 | 
			
		||||
{
 | 
			
		||||
@ -18,7 +19,7 @@ namespace Common
 | 
			
		||||
 | 
			
		||||
        private static readonly Random rnd = new Random();
 | 
			
		||||
 | 
			
		||||
        public static SpawnedServerInfo SpawnServer(int players, string modName)
 | 
			
		||||
        public static SpawnedServerInfo SpawnServer(int players, string modName, string tempFolderName = "")
 | 
			
		||||
        {
 | 
			
		||||
            int serversRunning = Process.GetProcessesByName(nBloodExecutable).Count();
 | 
			
		||||
            if (serversRunning >= maximumServers)
 | 
			
		||||
@ -27,7 +28,7 @@ namespace Common
 | 
			
		||||
            Mod mod = GetModByName(modName);
 | 
			
		||||
            int port = GetPort();
 | 
			
		||||
 | 
			
		||||
            var process = Process.Start(GetProcessStartInfo(players, port, mod));
 | 
			
		||||
            var process = Process.Start(GetProcessStartInfo(players, port, mod, tempFolderName));
 | 
			
		||||
            return new SpawnedServerInfo(process, port, mod);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -37,7 +38,7 @@ namespace Common
 | 
			
		||||
                return Constants.SupportedMods["BLOOD"];
 | 
			
		||||
 | 
			
		||||
            if (!Constants.SupportedMods.ContainsKey(modName.ToUpper()))
 | 
			
		||||
                throw new Exception("This mod is not supported: " + modName);
 | 
			
		||||
                throw new Exception($"This mod is not supported: {modName}.");
 | 
			
		||||
 | 
			
		||||
            return Constants.SupportedMods[modName.ToUpper()];
 | 
			
		||||
        }
 | 
			
		||||
@ -59,9 +60,11 @@ namespace Common
 | 
			
		||||
            return port;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static ProcessStartInfo GetProcessStartInfo(int maxPlayers, int port, Mod mod)
 | 
			
		||||
        private static ProcessStartInfo GetProcessStartInfo(int maxPlayers, int port, Mod mod, string tempFolderName = "")
 | 
			
		||||
        {
 | 
			
		||||
            var psi = new ProcessStartInfo(GetExecutableName(), $"-server {maxPlayers} -port {port} -pname Server {mod.CommandLine}")
 | 
			
		||||
            string cmd = GetCommand(maxPlayers, port, mod, tempFolderName);
 | 
			
		||||
 | 
			
		||||
            var psi = new ProcessStartInfo(GetExecutableName(), cmd)
 | 
			
		||||
            {
 | 
			
		||||
                UseShellExecute = true,
 | 
			
		||||
                WorkingDirectory = CommandLineUtils.BloodDir
 | 
			
		||||
@ -70,6 +73,18 @@ namespace Common
 | 
			
		||||
            return psi;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static string GetCommand(int maxPlayers, int port, Mod mod, string tempFolderName)
 | 
			
		||||
        {
 | 
			
		||||
            string cmd = $"-server {maxPlayers} -port {port} -pname Server {mod.CommandLine}";
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(tempFolderName))
 | 
			
		||||
            {
 | 
			
		||||
                string tempFolderPath = Path.Combine(CommandLineUtils.TempMapDir, tempFolderName);
 | 
			
		||||
                cmd += $" -j={tempFolderPath}";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return cmd;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static string GetExecutableName()
 | 
			
		||||
        {
 | 
			
		||||
            string nbloodServer = nBloodExecutable;
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,8 @@ using System.Linq;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using Common;
 | 
			
		||||
 | 
			
		||||
namespace Supervisor
 | 
			
		||||
{
 | 
			
		||||
@ -16,7 +18,8 @@ namespace Supervisor
 | 
			
		||||
            {
 | 
			
		||||
                while (true)
 | 
			
		||||
                {
 | 
			
		||||
                    Thread.Sleep(TimeSpan.FromSeconds(2));
 | 
			
		||||
                    TempMapFolderCleanup();
 | 
			
		||||
                    Thread.Sleep(TimeSpan.FromSeconds(10));
 | 
			
		||||
                    KillUnusedServers();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
@ -39,7 +42,30 @@ namespace Supervisor
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                //Log...
 | 
			
		||||
                // Log...
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static void TempMapFolderCleanup()
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (!Directory.Exists(CommandLineUtils.TempMapDir))
 | 
			
		||||
                {
 | 
			
		||||
                    Directory.CreateDirectory(CommandLineUtils.TempMapDir);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                foreach (var dir in Directory.GetDirectories(CommandLineUtils.TempMapDir))
 | 
			
		||||
                {
 | 
			
		||||
                    if (DateTime.UtcNow - File.GetCreationTimeUtc(dir) > TimeSpan.FromDays(1))
 | 
			
		||||
                    {
 | 
			
		||||
                        Directory.Delete(dir, recursive: true);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                // Log...
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,8 @@ namespace WebInterface.Controllers
 | 
			
		||||
    [ApiController]
 | 
			
		||||
    public class NBloodController : ControllerBase
 | 
			
		||||
    {
 | 
			
		||||
        private const string generalErrorMessage = "Unhandled exception has been occured. Check the server logs for details.";
 | 
			
		||||
 | 
			
		||||
        private static bool _isBusy = false;
 | 
			
		||||
        private static DateTime _lastRefresh = DateTime.MinValue;
 | 
			
		||||
        private static ListServersResponse _lastServerList = null;
 | 
			
		||||
@ -71,7 +73,7 @@ namespace WebInterface.Controllers
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogError(ex.ToString());
 | 
			
		||||
                return new StartServerResponse("Unhandled exception has been occured. Check the logs for details.");
 | 
			
		||||
                return new StartServerResponse(generalErrorMessage);
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
@ -97,7 +99,7 @@ namespace WebInterface.Controllers
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogError(ex.ToString());
 | 
			
		||||
                return new ListServersResponse("Unhandled exception has been occured. Check the logs for details.");
 | 
			
		||||
                return new ListServersResponse(generalErrorMessage);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,10 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Common;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
using WebInterface.Infrastructure;
 | 
			
		||||
using WebInterface.Services;
 | 
			
		||||
 | 
			
		||||
namespace WebInterface.Controllers
 | 
			
		||||
@ -12,11 +13,18 @@ namespace WebInterface.Controllers
 | 
			
		||||
    {
 | 
			
		||||
        private readonly IPrivateServerService _privateServerService;
 | 
			
		||||
        private readonly IRateLimiterService _rateLimiterService;
 | 
			
		||||
        private readonly ICustomMapService _customMapService;
 | 
			
		||||
        private readonly ILogger<PrivateController> _logger;
 | 
			
		||||
 | 
			
		||||
        public PrivateController(IPrivateServerService privateServerService, IRateLimiterService rateLimiterService)
 | 
			
		||||
        public PrivateController(IPrivateServerService privateServerService,
 | 
			
		||||
            IRateLimiterService rateLimiterService,
 | 
			
		||||
            ICustomMapService customMapService,
 | 
			
		||||
            ILogger<PrivateController> logger)
 | 
			
		||||
        {
 | 
			
		||||
            _privateServerService = privateServerService;
 | 
			
		||||
            _rateLimiterService = rateLimiterService;
 | 
			
		||||
            _customMapService = customMapService;
 | 
			
		||||
            _logger = logger;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Route("nblood/private")]
 | 
			
		||||
@ -41,22 +49,31 @@ namespace WebInterface.Controllers
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                if (!ModelState.IsValid)
 | 
			
		||||
                    throw new Exception("Something went off the rails.");
 | 
			
		||||
                    throw new WebInterfaceException("Something went off the rails.");
 | 
			
		||||
 | 
			
		||||
                if (!_rateLimiterService.IsRequestAllowed(HttpContext.Connection.RemoteIpAddress))
 | 
			
		||||
                    throw new Exception("Sorry, you have requested too many servers recently, you need to wait some time.");
 | 
			
		||||
                    throw new WebInterfaceException("Sorry, you have requested too many servers recently, you need to wait some time.");
 | 
			
		||||
 | 
			
		||||
                var spawnedServer = _privateServerService.SpawnNewPrivateServer(request.Players + 1, request.ModName);
 | 
			
		||||
                string tempFolderName = "";
 | 
			
		||||
                if ((request.FormFile?.Length ?? 0) > 0)
 | 
			
		||||
                    tempFolderName = _customMapService.StoreTempCustomMap(request.FormFile);
 | 
			
		||||
 | 
			
		||||
                var spawnedServer = _privateServerService.SpawnNewPrivateServer(request.Players + 1, request.ModName, tempFolderName ?? "");
 | 
			
		||||
                string commandLine = CommandLineUtils.GetClientLaunchCommand(HttpContext.Request.Host.Host,
 | 
			
		||||
                    spawnedServer.Port,
 | 
			
		||||
                    spawnedServer.Mod.CommandLine);
 | 
			
		||||
 | 
			
		||||
                viewModel = new StartServerResponse(spawnedServer.Port, commandLine);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            catch (WebInterfaceException ex)
 | 
			
		||||
            {
 | 
			
		||||
                viewModel = new StartServerResponse(ex.Message);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                _logger.LogError(ex.ToString());
 | 
			
		||||
                viewModel = new StartServerResponse("Internal server error.");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return View("Result", viewModel);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										19
									
								
								WebInterface/Infrastructure/WebInterfaceException.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								WebInterface/Infrastructure/WebInterfaceException.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace WebInterface.Infrastructure
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    [Serializable]
 | 
			
		||||
    public class WebInterfaceException : Exception
 | 
			
		||||
    {
 | 
			
		||||
        public WebInterfaceException() { }
 | 
			
		||||
        public WebInterfaceException(string message) : base(message) { }
 | 
			
		||||
        public WebInterfaceException(string message, Exception inner) : base(message, inner) { }
 | 
			
		||||
        protected WebInterfaceException(
 | 
			
		||||
          System.Runtime.Serialization.SerializationInfo info,
 | 
			
		||||
          System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,4 +1,6 @@
 | 
			
		||||
using Common;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc.Rendering;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
@ -7,6 +9,8 @@ using System.Linq;
 | 
			
		||||
 | 
			
		||||
namespace WebInterface
 | 
			
		||||
{
 | 
			
		||||
    [RequestFormLimits(MultipartBodyLengthLimit = Constants.FileSizeLimit)]
 | 
			
		||||
    [RequestSizeLimit(Constants.FileSizeLimit)]
 | 
			
		||||
    public class PrivateViewModel
 | 
			
		||||
    {
 | 
			
		||||
        [Required]
 | 
			
		||||
@ -18,5 +22,8 @@ namespace WebInterface
 | 
			
		||||
        public List<SelectListItem> ModNames { get; } = Constants.SupportedMods
 | 
			
		||||
            .Select(m => new SelectListItem(m.Value.FriendlyName, m.Value.Name))
 | 
			
		||||
            .ToList();
 | 
			
		||||
 | 
			
		||||
        [Display(Name = "Custom map (optional)")]
 | 
			
		||||
        public IFormFile FormFile { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Common;
 | 
			
		||||
using Microsoft.AspNetCore.Hosting;
 | 
			
		||||
using Microsoft.Extensions.Configuration;
 | 
			
		||||
using Microsoft.Extensions.Hosting;
 | 
			
		||||
@ -20,7 +21,8 @@ namespace WebInterface
 | 
			
		||||
            Host.CreateDefaultBuilder(args)
 | 
			
		||||
                .ConfigureWebHostDefaults(webBuilder =>
 | 
			
		||||
                {
 | 
			
		||||
                    webBuilder.UseStartup<Startup>();
 | 
			
		||||
                    webBuilder.UseStartup<Startup>()
 | 
			
		||||
                    .ConfigureKestrel((context, options) => options.Limits.MaxRequestBodySize = Constants.FileSizeLimit);
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,11 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Common;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using WebInterface.Infrastructure;
 | 
			
		||||
 | 
			
		||||
namespace WebInterface.Services
 | 
			
		||||
{
 | 
			
		||||
@ -26,7 +29,7 @@ namespace WebInterface.Services
 | 
			
		||||
            "CPBB04.MAP",
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        private List<string> ListableCustomMaps => Directory.GetFiles(Common.CommandLineUtils.BloodDir,
 | 
			
		||||
        private List<string> ListableCustomMaps => Directory.GetFiles(CommandLineUtils.BloodDir,
 | 
			
		||||
            "*.map", SearchOption.TopDirectoryOnly)
 | 
			
		||||
            .Select(m => Path.GetFileName(m))
 | 
			
		||||
            .Where(m => !ContainsString(crypticMaps, m))
 | 
			
		||||
@ -38,11 +41,11 @@ namespace WebInterface.Services
 | 
			
		||||
        {
 | 
			
		||||
            if (ListableCustomMaps.Any(m => StringsAreSame(m, map)))
 | 
			
		||||
            {
 | 
			
		||||
                return File.ReadAllBytes(Path.Combine(Common.CommandLineUtils.BloodDir, map));
 | 
			
		||||
                return File.ReadAllBytes(Path.Combine(CommandLineUtils.BloodDir, map));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                throw new Exception($"Cannot download this map: {map}");
 | 
			
		||||
                throw new WebInterfaceException($"Cannot download this map: {map}.");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -51,5 +54,37 @@ namespace WebInterface.Services
 | 
			
		||||
 | 
			
		||||
        private bool StringsAreSame(string left, string right) =>
 | 
			
		||||
            string.Compare(left, right, StringComparison.OrdinalIgnoreCase) == 0;
 | 
			
		||||
 | 
			
		||||
        public string StoreTempCustomMap(IFormFile formFile)
 | 
			
		||||
        {
 | 
			
		||||
            string filename = Path.GetFileNameWithoutExtension(formFile.FileName);
 | 
			
		||||
            ValidateFilename(filename);
 | 
			
		||||
 | 
			
		||||
            string tempFolderName = Path.GetRandomFileName();
 | 
			
		||||
            string mapPath = Path.Combine(CommandLineUtils.TempMapDir, tempFolderName);
 | 
			
		||||
            if (!Directory.Exists(mapPath))
 | 
			
		||||
                Directory.CreateDirectory(mapPath);
 | 
			
		||||
 | 
			
		||||
            mapPath = Path.Combine(mapPath, filename + ".map");
 | 
			
		||||
            FileStream fs = new FileStream(mapPath, FileMode.CreateNew);
 | 
			
		||||
            formFile.CopyTo(fs);
 | 
			
		||||
 | 
			
		||||
            return tempFolderName;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void ValidateFilename(string filename)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(filename))
 | 
			
		||||
                throw new WebInterfaceException("Invalid filename.");
 | 
			
		||||
 | 
			
		||||
            if (ContainsString(crypticMaps, filename + ".map"))
 | 
			
		||||
                throw new WebInterfaceException($"You cannot play this map ({filename}) as a custom map.");
 | 
			
		||||
 | 
			
		||||
            foreach (var chr in Path.GetInvalidFileNameChars())
 | 
			
		||||
            {
 | 
			
		||||
                if (filename.Contains(chr))
 | 
			
		||||
                    throw new WebInterfaceException("Invalid characters in the file name of the custom map.");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Microsoft.AspNetCore.Http;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
@ -9,5 +10,6 @@ namespace WebInterface.Services
 | 
			
		||||
    {
 | 
			
		||||
        IList<string> ListCustomMaps();
 | 
			
		||||
        byte[] GetCustomMapBytes(string map);
 | 
			
		||||
        string StoreTempCustomMap(IFormFile formFile);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,6 @@ namespace WebInterface.Services
 | 
			
		||||
{
 | 
			
		||||
    public interface IPrivateServerService
 | 
			
		||||
    {
 | 
			
		||||
        SpawnedServerInfo SpawnNewPrivateServer(int players, string modName);
 | 
			
		||||
        SpawnedServerInfo SpawnNewPrivateServer(int players, string modName, string tempFolderName = "");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,11 +15,11 @@ namespace WebInterface.Services
 | 
			
		||||
        private static readonly Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
 | 
			
		||||
        private static readonly IPEndPoint webApiListenerEndPoint = new IPEndPoint(IPAddress.Loopback, 11028);
 | 
			
		||||
 | 
			
		||||
        public SpawnedServerInfo SpawnNewPrivateServer(int players, string modName)
 | 
			
		||||
        public SpawnedServerInfo SpawnNewPrivateServer(int players, string modName, string tempFolderName = "")
 | 
			
		||||
        {
 | 
			
		||||
            players = Math.Min(8, Math.Max(3, players));
 | 
			
		||||
 | 
			
		||||
            SpawnedServerInfo serverProcess = ProcessSpawner.SpawnServer(players, modName);
 | 
			
		||||
            SpawnedServerInfo serverProcess = ProcessSpawner.SpawnServer(players, modName, tempFolderName);
 | 
			
		||||
            byte[] payload = Encoding.ASCII.GetBytes($"B{serverProcess.Port}\t{serverProcess.Process.Id}\0");
 | 
			
		||||
            socket.SendTo(payload, webApiListenerEndPoint);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,24 +3,27 @@ using System.Collections.Concurrent;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Security.Cryptography;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace WebInterface.Services
 | 
			
		||||
{
 | 
			
		||||
    public class RateLimiterService : IRateLimiterService
 | 
			
		||||
    {
 | 
			
		||||
        private static readonly ConcurrentDictionary<IPAddress, List<DateTime>> requestHistory = new ConcurrentDictionary<IPAddress, List<DateTime>>();
 | 
			
		||||
        private static readonly ConcurrentDictionary<string, List<DateTime>> requestHistory = new ConcurrentDictionary<string, List<DateTime>>();
 | 
			
		||||
 | 
			
		||||
        public bool IsRequestAllowed(IPAddress remoteIpAddress)
 | 
			
		||||
        {
 | 
			
		||||
            if (requestHistory.TryGetValue(remoteIpAddress, out var list))
 | 
			
		||||
            string hash = GetHash(remoteIpAddress);
 | 
			
		||||
            if (requestHistory.TryGetValue(hash, out var list))
 | 
			
		||||
            {
 | 
			
		||||
                 bool isLimited = list.Count(e => DateTime.UtcNow - e < TimeSpan.FromHours(1)) >= 5;
 | 
			
		||||
                if (isLimited)
 | 
			
		||||
                    return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            requestHistory.AddOrUpdate(remoteIpAddress,
 | 
			
		||||
            requestHistory.AddOrUpdate(hash,
 | 
			
		||||
                (ip) =>
 | 
			
		||||
                {
 | 
			
		||||
                    var list = new List<DateTime>();
 | 
			
		||||
@ -35,5 +38,25 @@ namespace WebInterface.Services
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string GetHash(IPAddress remoteIpAddress)
 | 
			
		||||
        {
 | 
			
		||||
            const string pepper = "Somewhere, over the rainbow, way up this pepper.";
 | 
			
		||||
            var data = Encoding.ASCII.GetBytes(remoteIpAddress.ToString() + pepper);
 | 
			
		||||
            string hash;
 | 
			
		||||
            using (SHA512 shaM = new SHA512Managed())
 | 
			
		||||
            {
 | 
			
		||||
                byte[] bytes = shaM.ComputeHash(data);
 | 
			
		||||
                for (int i = 0; i < 1000; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    bytes = shaM.ComputeHash(bytes);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                bytes = bytes.Take(bytes.Length / 4).ToArray();
 | 
			
		||||
                hash = Encoding.ASCII.GetString(bytes);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return hash;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,8 +3,11 @@ using System.Diagnostics;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using System.Runtime.InteropServices;
 | 
			
		||||
using Common;
 | 
			
		||||
using Microsoft.AspNetCore.Builder;
 | 
			
		||||
using Microsoft.AspNetCore.Hosting;
 | 
			
		||||
using Microsoft.AspNetCore.Http.Features;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Microsoft.Extensions.Configuration;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
using Microsoft.Extensions.Hosting;
 | 
			
		||||
@ -28,7 +31,7 @@ namespace WebInterface
 | 
			
		||||
                supervisor += ".exe";
 | 
			
		||||
            string supervisorPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), supervisor);
 | 
			
		||||
            if (!File.Exists(supervisorPath))
 | 
			
		||||
                throw new Exception($"Couldn't find {supervisor} at {supervisorPath}");
 | 
			
		||||
                throw new Exception($"Couldn't find {supervisor} at {supervisorPath}.");
 | 
			
		||||
            Process.Start(supervisorPath);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -43,6 +46,29 @@ namespace WebInterface
 | 
			
		||||
            services.Add(new ServiceDescriptor(typeof(IPrivateServerService), typeof(PrivateServerService), ServiceLifetime.Transient));
 | 
			
		||||
            services.Add(new ServiceDescriptor(typeof(IRateLimiterService), typeof(RateLimiterService), ServiceLifetime.Singleton));
 | 
			
		||||
            services.Add(new ServiceDescriptor(typeof(ICustomMapService), typeof(CustomMapService), ServiceLifetime.Transient));
 | 
			
		||||
 | 
			
		||||
            services.Configure<FormOptions>(options =>
 | 
			
		||||
            {
 | 
			
		||||
                // Set the limit to 1 MB
 | 
			
		||||
                options.MultipartBodyLengthLimit = Constants.FileSizeLimit;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            services.AddRazorPages()
 | 
			
		||||
                .AddRazorPagesOptions(options =>
 | 
			
		||||
                {
 | 
			
		||||
                    options.Conventions
 | 
			
		||||
                        .AddPageApplicationModelConvention("/nblood/private", model =>
 | 
			
		||||
                        {
 | 
			
		||||
                            model.Filters.Add(
 | 
			
		||||
                                new RequestFormLimitsAttribute()
 | 
			
		||||
                                {
 | 
			
		||||
                                    // Set the limit to 1 MB
 | 
			
		||||
                                    MultipartBodyLengthLimit = Constants.FileSizeLimit
 | 
			
		||||
                                });
 | 
			
		||||
                            model.Filters.Add(
 | 
			
		||||
                                new RequestSizeLimitAttribute(Constants.FileSizeLimit));
 | 
			
		||||
                        });
 | 
			
		||||
                });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,12 @@
 | 
			
		||||
 | 
			
		||||
<h1>List of available custom maps</h1>
 | 
			
		||||
 | 
			
		||||
<p>You can play these custom maps even on the public servers.
 | 
			
		||||
 If you want to play on your own map which is not listed here,
 | 
			
		||||
 you can request a new private server and upload your map which will be
 | 
			
		||||
 only available for that match, and the map won't appear in the below list.
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
<ul>
 | 
			
		||||
    @foreach (var map in Model)
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -6,11 +6,33 @@
 | 
			
		||||
 | 
			
		||||
<h1>@ViewData["Title"]</h1>
 | 
			
		||||
<div class="bordered" style="padding:4px">
 | 
			
		||||
    <form method="post">
 | 
			
		||||
        <label asp-for="ModName"></label>
 | 
			
		||||
        <select asp-for="ModName" asp-items="Model.ModNames"></select> <br />
 | 
			
		||||
        <label asp-for="Players"></label>
 | 
			
		||||
        <input type="number" min="2" max="7" asp-for="Players" /> <br />
 | 
			
		||||
    <form enctype="multipart/form-data" method="post">
 | 
			
		||||
        <dl>
 | 
			
		||||
            <dt>
 | 
			
		||||
                <label asp-for="ModName"></label>
 | 
			
		||||
            </dt>
 | 
			
		||||
            <dd>
 | 
			
		||||
                <select asp-for="ModName" asp-items="Model.ModNames"></select>
 | 
			
		||||
            </dd>
 | 
			
		||||
        </dl>
 | 
			
		||||
 | 
			
		||||
        <dl>
 | 
			
		||||
            <dt>
 | 
			
		||||
                <label asp-for="Players"></label>
 | 
			
		||||
            </dt>
 | 
			
		||||
            <dd>
 | 
			
		||||
                <input type="number" min="2" max="7" asp-for="Players" />
 | 
			
		||||
            </dd>
 | 
			
		||||
        </dl>
 | 
			
		||||
 | 
			
		||||
        <dl>
 | 
			
		||||
            <dt>
 | 
			
		||||
                <label asp-for="FormFile"></label>
 | 
			
		||||
            </dt>
 | 
			
		||||
            <dd>
 | 
			
		||||
                <input asp-for="FormFile" type="file" accept=".map">
 | 
			
		||||
            </dd>
 | 
			
		||||
        </dl>
 | 
			
		||||
        <button asp-route="RequestPrivateServer">Request</button>
 | 
			
		||||
    </form>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,8 @@
 | 
			
		||||
    @if (Model.IsSuccess)
 | 
			
		||||
    {
 | 
			
		||||
        <p>Command: <span class="code">@Model.CommandLine</span></p>
 | 
			
		||||
        <p>You have 10 minutes to join.</p>
 | 
			
		||||
        <p>At least 1 person must join in 10 minutes, otherwise the server will be killed.</p>
 | 
			
		||||
        <p>If you have provided a custom map, you have to type its name in the user map section when starting a new game.</p>
 | 
			
		||||
    }
 | 
			
		||||
    else
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user