mirror of
https://gitea.tendokyu.moe/ppc/amnet.git
synced 2026-09-28 01:37:56 +03:00
move card storage/web server handlers to separate files
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace AMNet.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a storage location to load/read cards from
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the currently active card without clearing it
|
||||
/// </summary>
|
||||
public StoredCard PeekActiveCard()
|
||||
{
|
||||
lock (_cardLock)
|
||||
{
|
||||
return _currentCard;
|
||||
}
|
||||
}
|
||||
|
||||
public StoredCard TakeActiveCard()
|
||||
{
|
||||
lock (_cardLock)
|
||||
{
|
||||
var card = _currentCard;
|
||||
_currentCard = null;
|
||||
|
||||
return card;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the card
|
||||
/// </summary>
|
||||
/// <param name="cardId">The card id to set</param>
|
||||
/// <param name="validFor">How long the card should be presented to the game for in milliseconds</param>
|
||||
/// <returns>Whether the card was set successfully</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+29
-149
@@ -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<CardReadRequest>();
|
||||
}
|
||||
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<CardPresenter>().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(';');
|
||||
}
|
||||
}
|
||||
@@ -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<CardPresenter>();
|
||||
|
||||
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<CardReadRequest>();
|
||||
}
|
||||
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<CardPresenter>();
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user