using System; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; 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 AimeNet; public static class DllMain { private const int ApiVersion = 1; private const string WebAddress = "http://amnet.ppc.moe"; private static ILogger _serverLogger; private static string[] _serverAddresses; private static string _serverName; private static (byte[] IdBytes, string OriginalId, long Expires)? _currentCard; private static readonly object CardLock = new(); static DllMain() { PInvoke.AllocConsole(); } [UnmanagedCallersOnly(EntryPoint = "aime_io_get_api_version")] public static ushort GetApiVersion() => 0x0100; [UnmanagedCallersOnly(EntryPoint = "aime_io_init")] public static int Init() { LoadConfiguration(); var builder = WebApplication.CreateSlimBuilder([]); 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.WebHost.UseUrls(_serverAddresses); builder.Services.AddCors(cors => { cors.AddDefaultPolicy(policy => { policy.AllowAnyMethod(); policy.AllowAnyOrigin(); policy.SetPreflightMaxAge(TimeSpan.FromHours(6)); }); }); builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); }); var app = builder.Build(); app.UseCors(); app.MapGet("/", () => Results.Ok(new SystemState(ApiVersion, _serverName))); app.MapPost("/card", async ctx => { CardReadRequest request; try { request = await ctx.Request.ReadFromJsonAsync(); } catch (JsonException e) { await using var writer = new StreamWriter(ctx.Response.Body, Encoding.UTF8, leaveOpen: true); await writer.WriteLineAsync(e.Message); ctx.Response.StatusCode = 400; return; } lock (CardLock) { // don't allow multiple writes in quick succession if (_currentCard?.Expires > Environment.TickCount64) { ctx.Response.StatusCode = 429; ctx.Response.Headers.RetryAfter = ((int)TimeSpan.FromTicks(_currentCard.Value.Expires - Environment.TickCount64).TotalSeconds).ToString(); return; } if (request.MatrixCode.Length < 20) { ctx.Response.StatusCode = 400; 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); } _currentCard = (bytes, matrixCode, Environment.TickCount64 + 5000000); } ctx.Response.StatusCode = 204; }); _serverLogger = app.Logger; CancellationTokenRegistration cancellationRegistration = default; cancellationRegistration = app.Lifetime.ApplicationStarted.Register(() => { _serverLogger.LogInformation("AimeNet started successfully."); _serverLogger.LogInformation("Visit {addr} from a mobile device on the same network to get started.", WebAddress); cancellationRegistration.Dispose(); }); 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; } [UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_get_aime_id")] public static int GetAimeId(byte unitNo, nint luid, nint luidSize) { lock (CardLock) { if (unitNo != 0 || _currentCard == null) { return 1; } Marshal.Copy(_currentCard.Value.IdBytes, 0, luid, (int)luidSize); _serverLogger.LogInformation("Aime Card read: {0}", string.Join(" ", Enumerable.Range(0, 5).Select(x => _currentCard.Value.OriginalId.Substring(x * 4, 4)))); _currentCard = null; } return 0; } [UnmanagedCallersOnly(EntryPoint = "aime_io_nfc_get_felica_id")] public static unsafe int GetFelicaId(byte unitNo, ulong* idm) { // felica not supported return 1; } [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 serverName = stackalloc char[64]; var serverNameStr = new PWSTR(serverName); var serverAddress = stackalloc char[1024]; var serverAddressStr = new PWSTR(serverAddress); PInvoke.GetPrivateProfileString("aimeio", "serverName", Environment.MachineName, serverNameStr, 64, configFileName); PInvoke.GetPrivateProfileString("aimeio", "serverAddress", "http://+:6070", serverAddressStr, 1024, configFileName); _serverName = serverNameStr.ToString(); _serverAddresses = serverAddressStr.ToString().Split(';'); } }