From ec98003d2c4ad48967af076e1ef12b797720a80d Mon Sep 17 00:00:00 2001 From: ppc Date: Mon, 2 Sep 2024 14:14:02 +0100 Subject: [PATCH] move card storage/web server handlers to separate files --- AMNet.Server/CardPresenter.cs | 73 ++++++++++++++ AMNet.Server/Config.cs | 31 ++++++ AMNet.Server/DllMain.cs | 178 ++++++---------------------------- AMNet.Server/WebServer.cs | 80 ++++++++++++++- 4 files changed, 211 insertions(+), 151 deletions(-) create mode 100644 AMNet.Server/CardPresenter.cs create mode 100644 AMNet.Server/Config.cs diff --git a/AMNet.Server/CardPresenter.cs b/AMNet.Server/CardPresenter.cs new file mode 100644 index 0000000..d1ef0f2 --- /dev/null +++ b/AMNet.Server/CardPresenter.cs @@ -0,0 +1,73 @@ +using System; +using System.Globalization; + +namespace AMNet.Server; + +/// +/// Represents a storage location to load/read cards from +/// +public class CardPresenter +{ + private readonly object _cardLock = new(); + private StoredCard _currentCard; + + public record StoredCard(byte[] Value, string OriginalValue, long ExpiresAt) + { + public bool Expired => Environment.TickCount64 >= ExpiresAt; + } + + /// + /// Returns the currently active card without clearing it + /// + public StoredCard PeekActiveCard() + { + lock (_cardLock) + { + return _currentCard; + } + } + + public StoredCard TakeActiveCard() + { + lock (_cardLock) + { + var card = _currentCard; + _currentCard = null; + + return card; + } + } + + /// + /// Sets the card + /// + /// The card id to set + /// How long the card should be presented to the game for in milliseconds + /// Whether the card was set successfully + public bool SetCard(string cardId, long validFor = 5000) + { + // ensure the matrix code is 20-digits long, otherwise pad with zeros + var matrixCode = cardId.PadLeft(20, '0'); + var bytes = new byte[10]; + + for (var i = 0; i < bytes.Length; i++) + { + var value = matrixCode.Substring(i * 2, 2); + if (byte.TryParse(value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var b)) + { + bytes[i] = b; + } + else + { + return false; + } + } + + lock (_cardLock) + { + _currentCard = new StoredCard(bytes, matrixCode, Environment.TickCount64 + validFor); + } + + return true; + } +} \ No newline at end of file diff --git a/AMNet.Server/Config.cs b/AMNet.Server/Config.cs new file mode 100644 index 0000000..7df4bb9 --- /dev/null +++ b/AMNet.Server/Config.cs @@ -0,0 +1,31 @@ +using System; +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace AMNet.Server; + +internal static class Config +{ + private const string ConfigFileName = @".\segatools.ini"; + + internal const string IOSection = "aimeio"; + + static Config() + { + GameId = ReadKey(IOSection, "gameId", 4); + ServerName = ReadKey(IOSection, "serverName", 26, Environment.MachineName); + } + + public static string GameId { get; } + public static string ServerName { get; } + + internal static unsafe string ReadKey(string section, string key, uint maxLength, string @default = null) + { + var buffer = stackalloc char[(int)maxLength + 1]; + var bufferStr = new PWSTR(buffer); + + PInvoke.GetPrivateProfileString(section, key, @default ?? string.Empty, bufferStr, maxLength, ConfigFileName); + + return bufferStr.ToString(); + } +} \ No newline at end of file diff --git a/AMNet.Server/DllMain.cs b/AMNet.Server/DllMain.cs index 65bb46c..491325d 100644 --- a/AMNet.Server/DllMain.cs +++ b/AMNet.Server/DllMain.cs @@ -1,32 +1,18 @@ -using System; -using System.Globalization; using System.Linq; using System.Runtime.InteropServices; -using System.Text.Json; using System.Threading; using Windows.Win32; -using Windows.Win32.Foundation; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace AMNet.Server; public static class DllMain { - private const int ApiVersion = 1; private const string WebAddress = "http://card.ppc.moe"; - private static ILogger _serverLogger; - - private static string[] _serverAddresses; - private static string _serverName; - private static string _gameId; - - private static (byte[] IdBytes, string OriginalId, long Expires)? _currentCard; - - private static readonly object _cardLock = new(); + private static WebApplication _app; static DllMain() { @@ -39,105 +25,27 @@ public static class DllMain [UnmanagedCallersOnly(EntryPoint = "aime_io_init")] public static int Init() { - LoadConfiguration(); + var addresses = Config.ReadKey(Config.IOSection, "serverAddress", 1024, "http://+:6070").Split(';'); - var builder = WebServer.BuildServer(); - builder.WebHost.UseUrls(_serverAddresses); + _app = WebServer.BuildServer(addresses); - var app = builder.Build(); - - _serverLogger = app.Logger; - - app.UseCors(); - app.MapGet("/amnet/info", () => Results.Ok(new SystemState(ApiVersion, _gameId, _serverName))); - app.MapPost("/amnet/signin", async ctx => + CancellationTokenRegistration? cancellationRegistration = null; + cancellationRegistration = _app.Lifetime.ApplicationStarted.Register(() => { - CardReadRequest request; + _app.Logger.LogInformation("AMNet Server ({gameId}) started successfully.", Config.GameId); + _app.Logger.LogInformation("Visit {addr} from a mobile device on the same network to get started.", WebAddress); - try - { - request = await ctx.Request.ReadFromJsonAsync(); - } - catch (JsonException e) - { - ctx.Response.StatusCode = 400; - await ctx.Response.WriteAsync(e.Message); - - return; - } - - if (string.IsNullOrEmpty(request?.MatrixCode)) - { - ctx.Response.StatusCode = 400; - await ctx.Response.WriteAsync("Card id not provided."); - return; - } - - if (request.MatrixCode.Length is 0 or < 20) - { - ctx.Response.StatusCode = 422; - await ctx.Response.WriteAsync("Invalid card id length."); - return; - } - - if (request.MatrixCode.Any(x => !char.IsNumber(x))) - { - ctx.Response.StatusCode = 422; - await ctx.Response.WriteAsync("Invalid card id format."); - return; - } - - lock (_cardLock) - { - // ratelimit check - if (_currentCard?.Expires > Environment.TickCount64) - { - ctx.Response.StatusCode = 429; - ctx.Response.Headers.RetryAfter = ((int)TimeSpan.FromMilliseconds(_currentCard.Value.Expires - Environment.TickCount64).TotalSeconds).ToString(); - return; - } - - // ensure the matrix code is 20-digits long, otherwise pad with zeros - var matrixCode = request.MatrixCode.PadLeft(20, '0'); - var bytes = new byte[10]; - - for (var i = 0; i < 10; i++) - { - var value = matrixCode.Substring(i * 2, 2); - bytes[i] = byte.Parse(value, NumberStyles.HexNumber); - } - - // it's called a tick count, but it uses milliseconds despite having a unit of time called a tick??? - _currentCard = (bytes, matrixCode, Environment.TickCount64 + 5000); - } - - ctx.Response.StatusCode = 202; + // ReSharper disable once AccessToModifiedClosure + cancellationRegistration?.Dispose(); }); - CancellationTokenRegistration cancellationRegistration = default; - cancellationRegistration = app.Lifetime.ApplicationStarted.Register(() => - { - _serverLogger.LogInformation("AMNet Server ({gameId}) started successfully.", _gameId); - _serverLogger.LogInformation("Visit {addr} from a mobile device on the same network to get started.", WebAddress); - - cancellationRegistration.Dispose(); - }); - - app.RunAsync(); + _app.RunAsync(); return 0; } [UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_poll")] public static int NfcPoll(byte unitNo) { - if (_currentCard.HasValue && _currentCard.Value.Expires < Environment.TickCount64) - { - lock (_cardLock) - { - _currentCard = null; - } - } - // there's no polling here (handled by the webserver) return 0; } @@ -145,62 +53,34 @@ public static class DllMain [UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_get_aime_id")] public static int GetAimeId(byte unitNo, nint luid, nint luidSize) { - lock (_cardLock) + if (unitNo != 0) { - if (unitNo != 0 || _currentCard == null) - { - return 1; - } - - if (_currentCard.Value.Expires < Environment.TickCount64) - { - _serverLogger.LogWarning("Submitted card id expired before it could be read."); - _currentCard = null; - - return 1; - } - - Marshal.Copy(_currentCard.Value.IdBytes, 0, luid, (int)luidSize); - - _serverLogger.LogInformation("Card read in: {0}", string.Join(" ", Enumerable.Range(0, 5).Select(x => _currentCard.Value.OriginalId.Substring(x * 4, 4)))); - _currentCard = null; + return 1; } - return 0; + var card = _app.Services.GetRequiredService().TakeActiveCard(); + if (card?.Expired != false) + { + return 1; + } + + try + { + Marshal.Copy(card.Value, 0, luid, (int)luidSize); + return 0; + } + finally + { + _app.Logger.LogInformation("Card read in: {0}", string.Join(" ", Enumerable.Range(0, 5).Select(x => card.OriginalValue.Substring(x * 4, 4)))); + } } [UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_get_felica_id")] - public static unsafe int GetFelicaId(byte unitNo, ulong* idm) - { - // felica not supported - return 1; - } + public static unsafe int GetFelicaId(byte unitNo, ulong* idm) => 1; // felica not supported [UnmanagedCallersOnly(EntryPoint = "aime_io_led_set_color")] public static void SetLedColour(byte unitNo, byte r, byte g, byte b) { // do nothing } - - private static unsafe void LoadConfiguration() - { - const string configFileName = @".\segatools.ini"; - - var gameId = stackalloc char[5]; - var gameIdStr = new PWSTR(gameId); - - var serverName = stackalloc char[64]; - var serverNameStr = new PWSTR(serverName); - - var serverAddress = stackalloc char[1024]; - var serverAddressStr = new PWSTR(serverAddress); - - PInvoke.GetPrivateProfileString("aimeio", "gameId", string.Empty, gameIdStr, 5, configFileName); - PInvoke.GetPrivateProfileString("aimeio", "serverName", Environment.MachineName, serverNameStr, 64, configFileName); - PInvoke.GetPrivateProfileString("aimeio", "serverAddress", "http://+:6070", serverAddressStr, 1024, configFileName); - - _gameId = gameIdStr.ToString(); - _serverName = serverNameStr.ToString(); - _serverAddresses = serverAddressStr.ToString().Split(';'); - } } \ No newline at end of file diff --git a/AMNet.Server/WebServer.cs b/AMNet.Server/WebServer.cs index 63dd5f5..88a1b02 100644 --- a/AMNet.Server/WebServer.cs +++ b/AMNet.Server/WebServer.cs @@ -1,5 +1,16 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text.Json; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -7,10 +18,14 @@ namespace AMNet.Server; internal static class WebServer { - public static WebApplicationBuilder BuildServer() + private const int ApiVersion = 1; + + public static WebApplication BuildServer(params string[] listenAddresses) { var builder = WebApplication.CreateSlimBuilder([]); + builder.WebHost.UseUrls(listenAddresses); + builder.Logging.ClearProviders(); builder.Logging.AddSimpleConsole(o => { @@ -39,6 +54,67 @@ internal static class WebServer options.SerializerOptions.TypeInfoResolverChain.Insert(0, SerializerContext.Default); }); - return builder; + builder.Services.AddSingleton(); + + var app = builder.Build(); + + app.UseCors(); + app.MapGet("/amnet/info", () => Results.Ok(new SystemState(ApiVersion, Config.GameId, Config.ServerName))); + app.MapPost("/amnet/signin", ProcessCard); + + return app; + } + + private static async Task ProcessCard(HttpContext ctx) + { + CardReadRequest request; + + try + { + request = await ctx.Request.ReadFromJsonAsync(); + } + catch (JsonException e) + { + ctx.Response.StatusCode = 400; + await ctx.Response.WriteAsync(e.Message); + + return; + } + + if (string.IsNullOrEmpty(request?.MatrixCode)) + { + ctx.Response.StatusCode = 400; + await ctx.Response.WriteAsync("Card id not provided."); + return; + } + + if (request.MatrixCode.Length is 0 or < 20) + { + ctx.Response.StatusCode = 422; + await ctx.Response.WriteAsync("Invalid card id length."); + return; + } + + var cardPresenter = ctx.RequestServices.GetRequiredService(); + var currentCard = cardPresenter.PeekActiveCard(); + + if (currentCard?.Expired == false) + { + var timeLeft = Math.Ceiling(TimeSpan.FromMilliseconds(currentCard.ExpiresAt - Environment.TickCount64).TotalSeconds); + + ctx.Response.StatusCode = 429; + ctx.Response.Headers.RetryAfter = timeLeft.ToString(CultureInfo.InvariantCulture); + return; + } + + if (cardPresenter.SetCard(request.MatrixCode)) + { + ctx.Response.StatusCode = 202; + } + else + { + ctx.Response.StatusCode = 422; + await ctx.Response.WriteAsync("Invalid card id format."); + } } } \ No newline at end of file