From 31c56d2449bc47732289bc713b8e0c9befcdeca4 Mon Sep 17 00:00:00 2001 From: ppc Date: Tue, 14 Jan 2025 15:46:35 +0000 Subject: [PATCH] update server to implement felica polling --- AMNet.Server/AMNet.Server.csproj | 1 - AMNet.Server/CardPresenter.cs | 27 ++++++++++++---- AMNet.Server/Config.cs | 29 ++++++++++------- AMNet.Server/DllMain.cs | 37 +++++++++++++-------- AMNet.Server/WebServer.cs | 55 +++++++++++++++++++------------- 5 files changed, 93 insertions(+), 56 deletions(-) diff --git a/AMNet.Server/AMNet.Server.csproj b/AMNet.Server/AMNet.Server.csproj index 8b6259d..1ee4c27 100644 --- a/AMNet.Server/AMNet.Server.csproj +++ b/AMNet.Server/AMNet.Server.csproj @@ -3,7 +3,6 @@ true Library - false net9.0-windows Size true diff --git a/AMNet.Server/CardPresenter.cs b/AMNet.Server/CardPresenter.cs index 2d053a4..b1c944d 100644 --- a/AMNet.Server/CardPresenter.cs +++ b/AMNet.Server/CardPresenter.cs @@ -13,11 +13,11 @@ internal class CardPresenter private readonly Lock _cardLock = new(); private StoredCard _currentCard; - public record StoredCard(byte[] Value, string OriginalValue, long ExpiresAt) + public record StoredCard(byte[] AccessCode, byte[] IDm, long ExpiresAt) { public bool Expired => Environment.TickCount64 >= ExpiresAt; - public override string ToString() => string.Join(" ", Enumerable.Range(0, 5).Select(x => OriginalValue.Substring(x * 4, 4))); + public override string ToString() => FormatHexCode(AccessCode); } /// @@ -31,10 +31,16 @@ internal class CardPresenter } } - public StoredCard TakeActiveCard() + public StoredCard TakeActiveCard(bool? requireIDm = null) { lock (_cardLock) { + // IDm and requireIDm are mutually exclusive + if (_currentCard == null || (requireIDm.HasValue && (_currentCard.IDm == null) ^ requireIDm.Value)) + { + return null; + } + var card = _currentCard; _currentCard = null; @@ -45,13 +51,14 @@ internal class CardPresenter /// /// Sets the card /// - /// The card id to set + /// The access code of the card to use + /// The FeliCa card IDm, if a physical card was used /// How long the card should be presented to the game for in milliseconds /// Whether the card was set successfully - public StoredCard SetCard(string cardId, long validFor = 5000) + public StoredCard SetCard(string accessCode, string idmHex, long validFor = 5000) { // ensure the matrix code is 20-digits long, otherwise pad with zeros - var matrixCode = cardId.Replace(" ", "").PadLeft(20, '0'); + var matrixCode = accessCode.Replace(" ", "").PadLeft(20, '0'); var bytes = new byte[10]; for (var i = 0; i < bytes.Length; i++) @@ -67,7 +74,8 @@ internal class CardPresenter } } - var card = new StoredCard(bytes, matrixCode, Environment.TickCount64 + validFor); + var idmBytes = !string.IsNullOrWhiteSpace(idmHex) && idmHex.Length <= 16 && idmHex.All(char.IsAsciiHexDigit) ? Convert.FromHexString(idmHex.PadLeft(16, '0')) : null; + var card = new StoredCard(bytes, idmBytes, Environment.TickCount64 + validFor); lock (_cardLock) { @@ -76,4 +84,9 @@ internal class CardPresenter return card; } + + /// + /// Formats a byte array as a hex string, inserting a space every 2 characters + /// + public static string FormatHexCode(byte[] code) => string.Join(" ", code.Chunk(2).Select(Convert.ToHexString)); } \ No newline at end of file diff --git a/AMNet.Server/Config.cs b/AMNet.Server/Config.cs index 506cf2c..8951085 100644 --- a/AMNet.Server/Config.cs +++ b/AMNet.Server/Config.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using Windows.Win32; using Windows.Win32.Foundation; @@ -10,30 +9,36 @@ internal static class Config internal const string IOSection = "aimeio"; private const string ConfigFileName = @".\segatools.ini"; - /// - /// Valid physical card identification methods. - /// IDm - uses the idm of the card to generate a 20-digit identifier (also known as the 0008 number) - /// Access - prefer the real access code of the card. if not available (i.e. Suica card), fallback to the IDm. - /// - private static readonly string[] ValidPhysicalCardIdentifiers = ["idm", "access"]; - static Config() { GameId = ReadKey(IOSection, "gameId", 4); ServerName = ReadKey(IOSection, "serverName", 26, Environment.MachineName); - - var physicalCardIdentifierPreference = ReadKey(IOSection, "physicalCardIdentifier", 10); - PhysicalCardIdentifierPreference = ValidPhysicalCardIdentifiers.Contains(physicalCardIdentifierPreference) ? physicalCardIdentifierPreference : ValidPhysicalCardIdentifiers[^1]; + PhysicalCardUseIDm = PInvoke.GetPrivateProfileInt(IOSection, "usePhysicalCardIdm", 0, ConfigFileName) > 0; EnableAimeTxt = PInvoke.GetPrivateProfileInt(IOSection, "enableKeyboardMode", 1, ConfigFileName) > 0; AimeTxtPath = ReadKey("aime", "aimePath", PInvoke.MAX_PATH, @"DEVICE\aime.txt"); } public static string GameId { get; } + + /// + /// The server name to show on devices. Defaults to the computer name. + /// public static string ServerName { get; } - public static string PhysicalCardIdentifierPreference { get; } + + /// + /// Whether physically scanned cards should have their IDm forwarded to the game instead of the access code. + /// + public static bool PhysicalCardUseIDm { get; } + /// + /// Whether holding the enter key should trigger a card scan using the code from the aime.txt file + /// public static bool EnableAimeTxt { get; } + + /// + /// Path to the aime.txt file. Defaults to .\DEVICE\aime.txt + /// public static string AimeTxtPath { get; } internal static unsafe string ReadKey(string section, string key, uint maxLength, string @default = null) diff --git a/AMNet.Server/DllMain.cs b/AMNet.Server/DllMain.cs index e951608..cda2438 100644 --- a/AMNet.Server/DllMain.cs +++ b/AMNet.Server/DllMain.cs @@ -36,7 +36,7 @@ public static class DllMain CancellationTokenRegistration? cancellationRegistration = null; cancellationRegistration = App.Lifetime.ApplicationStarted.Register(() => { - App.Logger.LogInformation("AMNet Server ({gameId}) started successfully.", Config.GameId); + App.Logger.LogInformation("AMNet Server ({gameId}) started successfully.", Config.GameId ?? "SXXX"); App.Logger.LogInformation("Visit {addr} from a mobile device on the same network to get started.", WebAddress); // ReSharper disable once AccessToModifiedClosure @@ -71,7 +71,7 @@ public static class DllMain using var aimeTxtReader = new StreamReader(File.OpenRead(Config.AimeTxtPath), Encoding.UTF8); var fileName = Path.GetFileName(Config.AimeTxtPath); - var cardResult = App.Services.GetRequiredService().SetCard(aimeTxtReader.ReadLine()); + var cardResult = App.Services.GetRequiredService().SetCard(aimeTxtReader.ReadLine(), null); if (cardResult == null) { @@ -98,25 +98,36 @@ public static class DllMain return 1; } - var card = App.Services.GetRequiredService().TakeActiveCard(); + // if physical cards are being directed to + var card = App.Services.GetRequiredService().TakeActiveCard(Config.PhysicalCardUseIDm ? false : null); if (card?.Expired != false) { return 1; } - try - { - Marshal.Copy(card.Value, 0, luid, (int)luidSize); - return 0; - } - finally - { - App.Logger.LogInformation("Card read in: {cardId}", card); - } + Marshal.Copy(card.AccessCode, 0, luid, (int)luidSize); + App.Logger.LogInformation("Access Code read: {cardId}", CardPresenter.FormatHexCode(card.AccessCode)); + return 0; } [UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_get_felica_id")] - public static unsafe int GetFelicaId(byte unitNo, ulong* idm) => 1; // felica not supported + public static int GetFelicaId(byte unitNo, IntPtr idm) + { + if (!Config.PhysicalCardUseIDm || unitNo != 0) + { + return 1; + } + + var card = App.Services.GetRequiredService().TakeActiveCard(true); + if (card?.Expired != false) + { + return 1; + } + + Marshal.Copy(card.IDm, 0, idm, sizeof(ulong)); + App.Logger.LogInformation("FeliCa IDm read: {idm}", card.IDm); + return 0; + } [UnmanagedCallersOnly(EntryPoint = "aime_io_led_set_color")] public static void SetLedColour(byte unitNo, byte r, byte g, byte b) diff --git a/AMNet.Server/WebServer.cs b/AMNet.Server/WebServer.cs index 18a0fc9..0e3d127 100644 --- a/AMNet.Server/WebServer.cs +++ b/AMNet.Server/WebServer.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; @@ -21,11 +22,14 @@ internal static partial class WebServer [property: JsonPropertyName("gameId")] string GameId, [property: JsonPropertyName("serverName")] string ServerName, [property: JsonPropertyName("sessionUptime")] long SessionUptime, - [property: JsonPropertyName("timeSinceLastPoll")] long? TimeSinceLastPoll, - [property: JsonPropertyName("physicalCardIdentifierPreference")] string PhysicalCardIdentifierPreference); + [property: JsonPropertyName("timeSinceLastPoll")] long? TimeSinceLastPoll); public record CardReadRequest( - [property: JsonPropertyName("cardId")] string MatrixCode); + [property: JsonPropertyName("cardId")] string AccessCode, + [property: JsonPropertyName("physicalCardIDm")] string PhysicalCardIDm); + + public record ServerErrorMessage( + [property: JsonPropertyName("message")] string Message); public static WebApplication BuildServer(params string[] listenAddresses) { @@ -83,8 +87,7 @@ internal static partial class WebServer Config.GameId, Config.ServerName, Environment.TickCount64 - startedAt, - lastPollAt < 0 ? null : Environment.TickCount64 - lastPollAt, - Config.PhysicalCardIdentifierPreference); + lastPollAt < 0 ? null : Environment.TickCount64 - lastPollAt); return Results.Ok(state); } @@ -99,23 +102,25 @@ internal static partial class WebServer } catch (JsonException e) { - ctx.Response.StatusCode = 400; - await ctx.Response.WriteAsync(e.Message); - + await WriteErrorResponse(ctx, StatusCodes.Status400BadRequest, e.Message); return; } - if (string.IsNullOrEmpty(request?.MatrixCode)) + if (string.IsNullOrEmpty(request?.AccessCode)) { - ctx.Response.StatusCode = 400; - await ctx.Response.WriteAsync("Card id not provided."); + await WriteErrorResponse(ctx, StatusCodes.Status400BadRequest, "Access Code not provided."); return; } - if (request.MatrixCode.Length is 0 or < 20) + if (request.AccessCode.Length is 0 or < 20 || !request.AccessCode.Any(char.IsAsciiDigit)) { - ctx.Response.StatusCode = 422; - await ctx.Response.WriteAsync("Invalid card id length."); + await WriteErrorResponse(ctx, StatusCodes.Status422UnprocessableEntity, "Invalid Access Code."); + return; + } + + if (request.AccessCode.All(x => x == '0')) + { + await WriteErrorResponse(ctx, StatusCodes.Status403Forbidden, "All-zero access codes are forbidden."); return; } @@ -124,26 +129,30 @@ internal static partial class WebServer 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); + ctx.Response.StatusCode = StatusCodes.Status429TooManyRequests; + ctx.Response.Headers.RetryAfter = Math.Ceiling(TimeSpan.FromMilliseconds(currentCard.ExpiresAt - Environment.TickCount64).TotalSeconds).ToString(CultureInfo.InvariantCulture); return; } - if (cardPresenter.SetCard(request.MatrixCode) != null) + if (cardPresenter.SetCard(request.AccessCode, request.PhysicalCardIDm) != null) { - ctx.Response.StatusCode = 202; + ctx.Response.StatusCode = StatusCodes.Status202Accepted; } else { - ctx.Response.StatusCode = 422; - await ctx.Response.WriteAsync("Invalid card id format."); + await WriteErrorResponse(ctx, StatusCodes.Status422UnprocessableEntity, "Invalid card id format."); } } - + + private static Task WriteErrorResponse(HttpContext ctx, int code, string message) + { + ctx.Response.StatusCode = code; + return ctx.Response.WriteAsJsonAsync(new ServerErrorMessage(message), SerializerContext.Default.ServerErrorMessage); + } + [JsonSerializable(typeof(SystemState))] [JsonSerializable(typeof(CardReadRequest))] + [JsonSerializable(typeof(ServerErrorMessage))] [JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.Never)] private partial class SerializerContext : JsonSerializerContext; } \ No newline at end of file