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