mirror of
https://gitea.tendokyu.moe/ppc/amnet.git
synced 2026-09-22 22:28:22 +03:00
158 lines
5.3 KiB
C#
158 lines
5.3 KiB
C#
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<CardPresenter>();
|
|
|
|
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<CardReadRequest>();
|
|
}
|
|
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<CardPresenter>();
|
|
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;
|
|
} |