using System; using System.Globalization; 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 MatrixCode); 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) { 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) != null) { ctx.Response.StatusCode = 202; } else { ctx.Response.StatusCode = 422; await ctx.Response.WriteAsync("Invalid card id format."); } } [JsonSerializable(typeof(SystemState))] [JsonSerializable(typeof(CardReadRequest))] [JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.Never)] private partial class SerializerContext : JsonSerializerContext; }