mirror of
https://github.com/CommonLoon102/NBloodServerSupervisor.git
synced 2024-12-22 18:52:44 +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 class CommandLineUtils
|
||||||
{
|
{
|
||||||
public static string BloodDir => Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "blood");
|
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) =>
|
public static string GetClientLaunchCommand(string host, int port, string modCommandLine) =>
|
||||||
$"nblood -client {host} -port {port} {modCommandLine}";
|
$"nblood -client {host} -port {port} {modCommandLine}";
|
||||||
|
@ -7,6 +7,8 @@ namespace Common
|
|||||||
{
|
{
|
||||||
public class Constants
|
public class Constants
|
||||||
{
|
{
|
||||||
|
public const long FileSizeLimit = 1024 * 1024;
|
||||||
|
|
||||||
public static readonly IReadOnlyDictionary<string, Mod> SupportedMods = new Dictionary<string, Mod>()
|
public static readonly IReadOnlyDictionary<string, Mod> SupportedMods = new Dictionary<string, Mod>()
|
||||||
{
|
{
|
||||||
{ "BLOOD", new Mod("BLOOD", "Blood", "") },
|
{ "BLOOD", new Mod("BLOOD", "Blood", "") },
|
||||||
|
@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.NetworkInformation;
|
using System.Net.NetworkInformation;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace Common
|
namespace Common
|
||||||
{
|
{
|
||||||
@ -18,7 +19,7 @@ namespace Common
|
|||||||
|
|
||||||
private static readonly Random rnd = new Random();
|
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();
|
int serversRunning = Process.GetProcessesByName(nBloodExecutable).Count();
|
||||||
if (serversRunning >= maximumServers)
|
if (serversRunning >= maximumServers)
|
||||||
@ -27,7 +28,7 @@ namespace Common
|
|||||||
Mod mod = GetModByName(modName);
|
Mod mod = GetModByName(modName);
|
||||||
int port = GetPort();
|
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);
|
return new SpawnedServerInfo(process, port, mod);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,7 +38,7 @@ namespace Common
|
|||||||
return Constants.SupportedMods["BLOOD"];
|
return Constants.SupportedMods["BLOOD"];
|
||||||
|
|
||||||
if (!Constants.SupportedMods.ContainsKey(modName.ToUpper()))
|
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()];
|
return Constants.SupportedMods[modName.ToUpper()];
|
||||||
}
|
}
|
||||||
@ -59,9 +60,11 @@ namespace Common
|
|||||||
return port;
|
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,
|
UseShellExecute = true,
|
||||||
WorkingDirectory = CommandLineUtils.BloodDir
|
WorkingDirectory = CommandLineUtils.BloodDir
|
||||||
@ -70,6 +73,18 @@ namespace Common
|
|||||||
return psi;
|
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()
|
private static string GetExecutableName()
|
||||||
{
|
{
|
||||||
string nbloodServer = nBloodExecutable;
|
string nbloodServer = nBloodExecutable;
|
||||||
|
@ -5,6 +5,8 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using Common;
|
||||||
|
|
||||||
namespace Supervisor
|
namespace Supervisor
|
||||||
{
|
{
|
||||||
@ -16,7 +18,8 @@ namespace Supervisor
|
|||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
Thread.Sleep(TimeSpan.FromSeconds(2));
|
TempMapFolderCleanup();
|
||||||
|
Thread.Sleep(TimeSpan.FromSeconds(10));
|
||||||
KillUnusedServers();
|
KillUnusedServers();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -39,7 +42,30 @@ namespace Supervisor
|
|||||||
}
|
}
|
||||||
catch
|
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]
|
[ApiController]
|
||||||
public class NBloodController : ControllerBase
|
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 bool _isBusy = false;
|
||||||
private static DateTime _lastRefresh = DateTime.MinValue;
|
private static DateTime _lastRefresh = DateTime.MinValue;
|
||||||
private static ListServersResponse _lastServerList = null;
|
private static ListServersResponse _lastServerList = null;
|
||||||
@ -71,7 +73,7 @@ namespace WebInterface.Controllers
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex.ToString());
|
_logger.LogError(ex.ToString());
|
||||||
return new StartServerResponse("Unhandled exception has been occured. Check the logs for details.");
|
return new StartServerResponse(generalErrorMessage);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -97,7 +99,7 @@ namespace WebInterface.Controllers
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex.ToString());
|
_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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Common;
|
using Common;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using WebInterface.Infrastructure;
|
||||||
using WebInterface.Services;
|
using WebInterface.Services;
|
||||||
|
|
||||||
namespace WebInterface.Controllers
|
namespace WebInterface.Controllers
|
||||||
@ -12,11 +13,18 @@ namespace WebInterface.Controllers
|
|||||||
{
|
{
|
||||||
private readonly IPrivateServerService _privateServerService;
|
private readonly IPrivateServerService _privateServerService;
|
||||||
private readonly IRateLimiterService _rateLimiterService;
|
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;
|
_privateServerService = privateServerService;
|
||||||
_rateLimiterService = rateLimiterService;
|
_rateLimiterService = rateLimiterService;
|
||||||
|
_customMapService = customMapService;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("nblood/private")]
|
[Route("nblood/private")]
|
||||||
@ -41,22 +49,31 @@ namespace WebInterface.Controllers
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
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))
|
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,
|
string commandLine = CommandLineUtils.GetClientLaunchCommand(HttpContext.Request.Host.Host,
|
||||||
spawnedServer.Port,
|
spawnedServer.Port,
|
||||||
spawnedServer.Mod.CommandLine);
|
spawnedServer.Mod.CommandLine);
|
||||||
|
|
||||||
viewModel = new StartServerResponse(spawnedServer.Port, commandLine);
|
viewModel = new StartServerResponse(spawnedServer.Port, commandLine);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (WebInterfaceException ex)
|
||||||
{
|
{
|
||||||
viewModel = new StartServerResponse(ex.Message);
|
viewModel = new StartServerResponse(ex.Message);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex.ToString());
|
||||||
|
viewModel = new StartServerResponse("Internal server error.");
|
||||||
|
}
|
||||||
|
|
||||||
return View("Result", viewModel);
|
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 Common;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -7,6 +9,8 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace WebInterface
|
namespace WebInterface
|
||||||
{
|
{
|
||||||
|
[RequestFormLimits(MultipartBodyLengthLimit = Constants.FileSizeLimit)]
|
||||||
|
[RequestSizeLimit(Constants.FileSizeLimit)]
|
||||||
public class PrivateViewModel
|
public class PrivateViewModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
@ -18,5 +22,8 @@ namespace WebInterface
|
|||||||
public List<SelectListItem> ModNames { get; } = Constants.SupportedMods
|
public List<SelectListItem> ModNames { get; } = Constants.SupportedMods
|
||||||
.Select(m => new SelectListItem(m.Value.FriendlyName, m.Value.Name))
|
.Select(m => new SelectListItem(m.Value.FriendlyName, m.Value.Name))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
[Display(Name = "Custom map (optional)")]
|
||||||
|
public IFormFile FormFile { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Common;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
@ -20,7 +21,8 @@ namespace WebInterface
|
|||||||
Host.CreateDefaultBuilder(args)
|
Host.CreateDefaultBuilder(args)
|
||||||
.ConfigureWebHostDefaults(webBuilder =>
|
.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.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using WebInterface.Infrastructure;
|
||||||
|
|
||||||
namespace WebInterface.Services
|
namespace WebInterface.Services
|
||||||
{
|
{
|
||||||
@ -26,7 +29,7 @@ namespace WebInterface.Services
|
|||||||
"CPBB04.MAP",
|
"CPBB04.MAP",
|
||||||
};
|
};
|
||||||
|
|
||||||
private List<string> ListableCustomMaps => Directory.GetFiles(Common.CommandLineUtils.BloodDir,
|
private List<string> ListableCustomMaps => Directory.GetFiles(CommandLineUtils.BloodDir,
|
||||||
"*.map", SearchOption.TopDirectoryOnly)
|
"*.map", SearchOption.TopDirectoryOnly)
|
||||||
.Select(m => Path.GetFileName(m))
|
.Select(m => Path.GetFileName(m))
|
||||||
.Where(m => !ContainsString(crypticMaps, m))
|
.Where(m => !ContainsString(crypticMaps, m))
|
||||||
@ -38,11 +41,11 @@ namespace WebInterface.Services
|
|||||||
{
|
{
|
||||||
if (ListableCustomMaps.Any(m => StringsAreSame(m, map)))
|
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
|
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) =>
|
private bool StringsAreSame(string left, string right) =>
|
||||||
string.Compare(left, right, StringComparison.OrdinalIgnoreCase) == 0;
|
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.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -9,5 +10,6 @@ namespace WebInterface.Services
|
|||||||
{
|
{
|
||||||
IList<string> ListCustomMaps();
|
IList<string> ListCustomMaps();
|
||||||
byte[] GetCustomMapBytes(string map);
|
byte[] GetCustomMapBytes(string map);
|
||||||
|
string StoreTempCustomMap(IFormFile formFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,6 @@ namespace WebInterface.Services
|
|||||||
{
|
{
|
||||||
public interface IPrivateServerService
|
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 Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||||
private static readonly IPEndPoint webApiListenerEndPoint = new IPEndPoint(IPAddress.Loopback, 11028);
|
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));
|
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");
|
byte[] payload = Encoding.ASCII.GetBytes($"B{serverProcess.Port}\t{serverProcess.Process.Id}\0");
|
||||||
socket.SendTo(payload, webApiListenerEndPoint);
|
socket.SendTo(payload, webApiListenerEndPoint);
|
||||||
|
|
||||||
|
@ -3,24 +3,27 @@ using System.Collections.Concurrent;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace WebInterface.Services
|
namespace WebInterface.Services
|
||||||
{
|
{
|
||||||
public class RateLimiterService : IRateLimiterService
|
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)
|
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;
|
bool isLimited = list.Count(e => DateTime.UtcNow - e < TimeSpan.FromHours(1)) >= 5;
|
||||||
if (isLimited)
|
if (isLimited)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
requestHistory.AddOrUpdate(remoteIpAddress,
|
requestHistory.AddOrUpdate(hash,
|
||||||
(ip) =>
|
(ip) =>
|
||||||
{
|
{
|
||||||
var list = new List<DateTime>();
|
var list = new List<DateTime>();
|
||||||
@ -35,5 +38,25 @@ namespace WebInterface.Services
|
|||||||
|
|
||||||
return true;
|
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.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using Common;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
@ -28,7 +31,7 @@ namespace WebInterface
|
|||||||
supervisor += ".exe";
|
supervisor += ".exe";
|
||||||
string supervisorPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), supervisor);
|
string supervisorPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), supervisor);
|
||||||
if (!File.Exists(supervisorPath))
|
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);
|
Process.Start(supervisorPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +46,29 @@ namespace WebInterface
|
|||||||
services.Add(new ServiceDescriptor(typeof(IPrivateServerService), typeof(PrivateServerService), ServiceLifetime.Transient));
|
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(IRateLimiterService), typeof(RateLimiterService), ServiceLifetime.Singleton));
|
||||||
services.Add(new ServiceDescriptor(typeof(ICustomMapService), typeof(CustomMapService), ServiceLifetime.Transient));
|
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.
|
// 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>
|
<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>
|
<ul>
|
||||||
@foreach (var map in Model)
|
@foreach (var map in Model)
|
||||||
{
|
{
|
||||||
|
@ -6,11 +6,33 @@
|
|||||||
|
|
||||||
<h1>@ViewData["Title"]</h1>
|
<h1>@ViewData["Title"]</h1>
|
||||||
<div class="bordered" style="padding:4px">
|
<div class="bordered" style="padding:4px">
|
||||||
<form method="post">
|
<form enctype="multipart/form-data" method="post">
|
||||||
<label asp-for="ModName"></label>
|
<dl>
|
||||||
<select asp-for="ModName" asp-items="Model.ModNames"></select> <br />
|
<dt>
|
||||||
<label asp-for="Players"></label>
|
<label asp-for="ModName"></label>
|
||||||
<input type="number" min="2" max="7" asp-for="Players" /> <br />
|
</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>
|
<button asp-route="RequestPrivateServer">Request</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,8 @@
|
|||||||
@if (Model.IsSuccess)
|
@if (Model.IsSuccess)
|
||||||
{
|
{
|
||||||
<p>Command: <span class="code">@Model.CommandLine</span></p>
|
<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
|
else
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user