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; }