diff --git a/AMNet.Server/AMNet.Server.csproj b/AMNet.Server/AMNet.Server.csproj deleted file mode 100644 index 85373f3..0000000 --- a/AMNet.Server/AMNet.Server.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - true - Library - net9.0-windows - Size - true - - - - AMNet - ppc - amnet - AMNet Server - Copyright (c) 2024-25 ppc - Server component for the AMNet IC Card switcher - - - - None - false - false - false - false - true - false - false - false - false - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - diff --git a/AMNet.Server/CardPresenter.cs b/AMNet.Server/CardPresenter.cs deleted file mode 100644 index b2d18fe..0000000 --- a/AMNet.Server/CardPresenter.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Threading; - -namespace AMNet.Server; - -/// -/// Represents a storage location to load/read cards from -/// -internal class CardPresenter -{ - private readonly Lock _cardLock = new(); - private StoredCard _currentCard; - - public record StoredCard(byte[] AccessCode, byte[] IDm, long ExpiresAt) - { - public bool Expired => Environment.TickCount64 >= ExpiresAt; - - public override string ToString() => FormatHexCode(AccessCode); - } - - /// - /// Returns the currently active card without clearing it - /// - public StoredCard PeekActiveCard() - { - lock (_cardLock) - { - return _currentCard; - } - } - - 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; - - return card; - } - } - - /// - /// Sets the card - /// - /// 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 accessCode, string idmHex, long validFor = 5000) - { - // ensure the matrix code is 20-digits long, otherwise pad with zeros - var matrixCode = accessCode.Replace(" ", "").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 null; - } - } - - 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) - { - _currentCard = card; - } - - 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 deleted file mode 100644 index 2213b18..0000000 --- a/AMNet.Server/Config.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Reflection; -using Windows.Win32; -using Windows.Win32.Foundation; - -namespace AMNet.Server; - -internal static class Config -{ - internal const string IOSection = "aimeio"; - - static Config() - { - GameId = ReadKey(IOSection, "gameId", 4); - ServerName = ReadKey(IOSection, "serverName", 26, Environment.MachineName); - - PhysicalCardUseIDm = PInvoke.GetPrivateProfileInt(IOSection, "useAimeDBForPhysicalCards", 0, ConfigFilePath) > 0; - EnableAimeTxt = PInvoke.GetPrivateProfileInt(IOSection, "enableKeyboardMode", 1, ConfigFilePath) > 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; } - - /// - /// 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; } - - public static Version ServerVersion { get; } = new(Assembly.GetExecutingAssembly().GetCustomAttribute()?.Version ?? "1.0"); - - private static string ConfigFilePath { get; } = Environment.GetEnvironmentVariable("SEGATOOLS_CONFIG_PATH") ?? ".\\segatools.ini"; - - internal static unsafe string ReadKey(string section, string key, uint maxLength, string @default = null) - { - // +1 for null terminator - var buffer = stackalloc char[(int)maxLength + 1]; - var bufferStr = new PWSTR(buffer); - - fixed (char* pSection = section) - fixed (char* pKey = key) - fixed (char* pDefault = @default ?? string.Empty) - fixed (char* pDefaultPath = ConfigFilePath) - { - PInvoke.GetPrivateProfileString(new PCWSTR(pSection), new PCWSTR(pKey), new PCWSTR(pDefault), bufferStr, maxLength + 1, new PCWSTR(pDefaultPath)); - } - - return bufferStr.ToString(); - } -} \ No newline at end of file diff --git a/AMNet.Server/DllMain.cs b/AMNet.Server/DllMain.cs deleted file mode 100644 index 099b46c..0000000 --- a/AMNet.Server/DllMain.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using Windows.Win32; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace AMNet.Server; - -public static class DllMain -{ - private const string WebAddress = "http://card.ppc.moe"; - - // instance metrics - internal static long LastPollTime = -1; - internal static long ServerStartedAt; - - static DllMain() - { - PInvoke.AllocConsole(); - - App = WebServer.BuildServer(Config.ReadKey(Config.IOSection, "serverAddress", 1024, "http://+:6070").Split(';')); - } - - private static WebApplication App { get; } - - [UnmanagedCallersOnly(EntryPoint = "aime_io_get_api_version")] - public static ushort GetApiVersion() => 0x0100; - - [UnmanagedCallersOnly(EntryPoint = "aime_io_init")] - public static int Init() - { - CancellationTokenRegistration? cancellationRegistration = null; - cancellationRegistration = App.Lifetime.ApplicationStarted.Register(() => - { - App.Logger.LogInformation("AMNet Server v{version} ({gameId}) started.", Config.ServerVersion.ToString(Config.ServerVersion.Build > 0 ? 3 : 2), Config.GameId ?? "SXXX"); - App.Logger.LogInformation("Visit {addr} from a mobile device on the same network to get started.", WebAddress); - - // ReSharper disable once AccessToModifiedClosure - cancellationRegistration?.Dispose(); - }); - - Volatile.Write(ref ServerStartedAt, Environment.TickCount64); - App.RunAsync(); - return 0; - } - - [UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_poll")] - public static int NfcPoll(byte unitNo) - { - Volatile.Write(ref LastPollTime, Environment.TickCount64); - - if (!Config.EnableAimeTxt) - { - return 1; - } - - // check keyboard input for the enter key being held down (0x0D) - var enterPressed = (PInvoke.GetAsyncKeyState(0x0D) & 0x8000) != 0; - - if (!enterPressed) - { - return 0; - } - - try - { - 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(), null); - - if (cardResult == null) - { - App.Logger.LogWarning("Failed to read card from {file}: invalid format", fileName); - } - else - { - App.Logger.LogInformation("Card loaded from {fileName}", fileName); - } - } - catch (IOException e) - { - App.Logger.LogWarning("Failed to read '{path}': {message}", Path.GetFullPath(Config.AimeTxtPath), e.Message); - } - - return 0; - } - - [UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_get_aime_id")] - public static int GetAimeId(byte unitNo, IntPtr luid, nint luidSize) - { - if (unitNo != 0) - { - return 1; - } - - // if physical cards are being directed to - var card = App.Services.GetRequiredService().TakeActiveCard(Config.PhysicalCardUseIDm ? false : null); - if (card?.Expired != false) - { - return 1; - } - - 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) - { - if (!Config.PhysicalCardUseIDm || unitNo != 0) - { - return 1; - } - - var card = App.Services.GetRequiredService().TakeActiveCard(true); - if (card?.Expired != false) - { - return 1; - } - - ulong idmValue = 0; - for (var i = 0 ; i < 8 ; i++) - { - idmValue = (idmValue << 8) | card.IDm[i]; - } - - *idm = idmValue; - - App.Logger.LogInformation("FeliCa IDm read: {idm}", CardPresenter.FormatHexCode(card.IDm)); - return 0; - } - - [UnmanagedCallersOnly(EntryPoint = "aime_io_led_set_color")] - public static void SetLedColour(byte unitNo, byte r, byte g, byte b) - { - // do nothing - } -} \ No newline at end of file diff --git a/AMNet.Server/NativeMethods.txt b/AMNet.Server/NativeMethods.txt deleted file mode 100644 index 7976195..0000000 --- a/AMNet.Server/NativeMethods.txt +++ /dev/null @@ -1,5 +0,0 @@ -GetPrivateProfileStringW -GetPrivateProfileInt -GetAsyncKeyState -AllocConsole -MAX_PATH diff --git a/AMNet.Server/WebServer.cs b/AMNet.Server/WebServer.cs deleted file mode 100644 index 0e3d127..0000000 --- a/AMNet.Server/WebServer.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace AMNet.Server; - -internal static partial class WebServer -{ - private const int ApiVersion = 1; - - public record SystemState( - [property: JsonPropertyName("apiVersion")] int ApiVersion, - [property: JsonPropertyName("gameId")] string GameId, - [property: JsonPropertyName("serverName")] string ServerName, - [property: JsonPropertyName("sessionUptime")] long SessionUptime, - [property: JsonPropertyName("timeSinceLastPoll")] long? TimeSinceLastPoll); - - public record CardReadRequest( - [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) - { - var builder = WebApplication.CreateSlimBuilder([]); - - builder.WebHost.UseUrls(listenAddresses); - - builder.Logging.ClearProviders(); - builder.Logging.AddSimpleConsole(o => - { - o.SingleLine = true; - o.IncludeScopes = false; - }); - - builder.Logging.SetMinimumLevel(LogLevel.Information); - builder.Logging.AddFilter("Microsoft.AspNetCore.Http.Result", LogLevel.None); - builder.Logging.AddFilter("Microsoft.AspNetCore.Routing.EndpointMiddleware", LogLevel.Error); - - builder.Services.AddCors(cors => - { - cors.AddDefaultPolicy(policy => - { - policy.AllowAnyMethod(); - policy.AllowAnyOrigin(); - policy.AllowAnyHeader(); - - policy.SetPreflightMaxAge(TimeSpan.FromHours(6)); - }); - }); - - builder.Services.ConfigureHttpJsonOptions(options => - { - options.SerializerOptions.TypeInfoResolverChain.Insert(0, SerializerContext.Default); - }); - - builder.Services.AddSingleton(); - - var app = builder.Build(); - - app.UseCors(); - - app.MapGet("/amnet/info", ServerInfo); - app.MapPost("/amnet/signin", ProcessCard); - - return app; - } - - private static IResult ServerInfo() - { - var startedAt = Volatile.Read(ref DllMain.ServerStartedAt); - var lastPollAt = Volatile.Read(ref DllMain.LastPollTime); - - var state = new SystemState( - ApiVersion, - Config.GameId, - Config.ServerName, - Environment.TickCount64 - startedAt, - lastPollAt < 0 ? null : Environment.TickCount64 - lastPollAt); - - return Results.Ok(state); - } - - private static async Task ProcessCard(HttpContext ctx) - { - CardReadRequest request; - - try - { - request = await ctx.Request.ReadFromJsonAsync(); - } - catch (JsonException e) - { - await WriteErrorResponse(ctx, StatusCodes.Status400BadRequest, e.Message); - return; - } - - if (string.IsNullOrEmpty(request?.AccessCode)) - { - await WriteErrorResponse(ctx, StatusCodes.Status400BadRequest, "Access Code not provided."); - return; - } - - if (request.AccessCode.Length is 0 or < 20 || !request.AccessCode.Any(char.IsAsciiDigit)) - { - 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; - } - - var cardPresenter = ctx.RequestServices.GetRequiredService(); - var currentCard = cardPresenter.PeekActiveCard(); - - if (currentCard?.Expired == false) - { - 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.AccessCode, request.PhysicalCardIDm) != null) - { - ctx.Response.StatusCode = StatusCodes.Status202Accepted; - } - else - { - 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 diff --git a/AMNet.sln b/AMNet.sln deleted file mode 100644 index 72058e5..0000000 --- a/AMNet.sln +++ /dev/null @@ -1,16 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AMNet.Server", "AMNet.Server\AMNet.Server.csproj", "{C9FA2545-F15C-4517-B3AB-55B8FC23FA04}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C9FA2545-F15C-4517-B3AB-55B8FC23FA04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C9FA2545-F15C-4517-B3AB-55B8FC23FA04}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C9FA2545-F15C-4517-B3AB-55B8FC23FA04}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C9FA2545-F15C-4517-B3AB-55B8FC23FA04}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal