Files
MeWeb/src/MeWeb.Server/ServerApi.cs
T

162 lines
6.7 KiB
C#
Raw Normal View History

using System.Net.Sockets;
using System.Text.Json.Serialization.Metadata;
using MeWeb.Core.Knowledge;
using MeWeb.Shared;
namespace MeWeb.Server;
public static class ServerApi
{
public static WebApplication MapMeWebApi(this WebApplication app)
{
app.MapGet(Endpoints.Server.GET_Info, Info);
app.MapGet(Endpoints.Server.GET_Ip, Ip);
app.MapGet(Endpoints.Downloads.GET_Page, Page);
app.MapGet(Endpoints.Downloads.GET_Privacy, Privacy);
app.MapGet(Endpoints.Downloads.GET_Platform, Download);
app.MapPost(Endpoints.Probe.POST_Ticket, Ticket);
app.MapPost(Endpoints.Probe.POST_Run, Run);
app.MapPost(Endpoints.Share.POST_Create, Share);
app.MapDelete(Endpoints.Share.DELETE_One, Unshare);
app.MapGet(Endpoints.Speed.GET_Down, Down);
app.MapPost(Endpoints.Speed.POST_Up, Up);
app.MapGet(Endpoints.Knowledge.GET_Manifest, KnowledgeManifest);
app.MapGet(Endpoints.Knowledge.GET_File, KnowledgeFile);
return app;
}
static IResult KnowledgeManifest(KnowledgeFeeder feeder) =>
feeder.Manifest() is { } manifest ? Results.Json(manifest, MeWebJsonContext.Default.ViewKnowledge) : Results.NotFound();
static IResult KnowledgeFile(string name, KnowledgeFeeder feeder)
{
if (!KnowledgeFeeder.Published.Contains(name)) return Results.NotFound();
var path = Path.Combine(feeder.Folder, "current", name);
if (!File.Exists(path)) return Results.NotFound();
var type = Path.GetExtension(name) switch
{
".json" => "application/json",
".txt" => "text/plain; charset=utf-8",
_ => "application/octet-stream",
};
return Results.File(path, type, name);
}
static async Task<IResult> Down(int? bytes, HttpContext context, SpeedService speed, CancellationToken ct)
{
var caller = Caller(context);
if (caller is null) return Results.BadRequest();
if (!speed.Allow(caller)) return Refused(context, new ViewRefusal { Reason = "too-many", RetryAfterSeconds = 3600 }, StatusCodes.Status400BadRequest);
if (!speed.Enter()) return Refused(context, new ViewRefusal { Reason = "busy", RetryAfterSeconds = 60 }, StatusCodes.Status400BadRequest);
try
{
var size = Math.Clamp(bytes ?? 1024 * 1024, 1024, Endpoints.Speed.MaxDownBytes);
await SpeedService.Stream(context.Response, size, ct);
return Results.Empty;
}
finally
{
speed.Leave();
}
}
static async Task<IResult> Up(HttpContext context, CancellationToken ct)
{
var received = await SpeedService.Drain(context.Request.Body, ct);
return received < 0 ? Refused(context, new ViewRefusal { Reason = "size" }, StatusCodes.Status400BadRequest) : Results.NoContent();
}
public const string DeleteTokenHeader = "X-Delete-Token";
static IResult Ticket(HttpContext context, ProbeService probe)
{
var caller = Caller(context);
if (caller is null) return Results.BadRequest();
return Answer(context, probe.Issue(caller), MeWebJsonContext.Default.ViewProbeTicket);
}
static async Task<IResult> Run(string id, ProbeForm form, HttpContext context, ProbeService probe, CancellationToken ct)
{
var caller = Caller(context);
if (caller is null) return Results.BadRequest();
return Answer(context, await probe.Run(id, caller, form, ct), MeWebJsonContext.Default.ViewProbe);
}
static async Task<IResult> Share(HttpContext context, ShareService shares, CancellationToken ct)
{
var caller = Caller(context);
if (caller is null) return Results.BadRequest();
using var buffer = new MemoryStream();
var chunk = new byte[8192];
var read = default(int);
while ((read = await context.Request.Body.ReadAsync(chunk, ct)) > 0)
{
buffer.Write(chunk, 0, read);
if (buffer.Length > ShareService.MaxBytes) return Refused(context, new ViewRefusal { Reason = "size" }, StatusCodes.Status400BadRequest);
}
return Answer(context, shares.Create(caller, buffer.ToArray()), MeWebJsonContext.Default.ViewShared);
}
static IResult Unshare(string id, HttpContext context, ShareService shares) =>
Results.StatusCode(shares.Delete(id, context.Request.Headers[DeleteTokenHeader].ToString()));
static IResult Answer<T>(HttpContext context, WebResult<T> result, JsonTypeInfo<T> type) where T : class =>
result.IsInvalid ? Refused(context, result.Refusal, result.StatusCode) : Results.Json(result.Data, type);
static IResult Refused(HttpContext context, ViewRefusal refusal, int status)
{
if (refusal.RetryAfterSeconds > 0) context.Response.Headers.RetryAfter = refusal.RetryAfterSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture);
return Results.Json(refusal, MeWebJsonContext.Default.ViewRefusal, statusCode: status);
}
static System.Net.IPAddress Caller(HttpContext context)
{
var address = context.Connection.RemoteIpAddress;
return address is null ? null : address.IsIPv4MappedToIPv6 ? address.MapToIPv4() : address;
}
static ViewInfo Info() => new() { Version = BuildInfo.Version, Commit = BuildInfo.Commit, BuiltAt = BuildInfo.BuiltAt };
static IResult Ip(HttpContext context, KnowledgeFeeder knowledge)
{
var address = context.Connection.RemoteIpAddress;
if (address is null) return Results.BadRequest();
if (address.IsIPv4MappedToIPv6) address = address.MapToIPv4();
var family = address.AddressFamily == AddressFamily.InterNetworkV6 ? "ipv6" : "ipv4";
var block = knowledge.BlockFor(address);
return Results.Ok(new ViewIp
{
Address = address.ToString(),
Family = family,
Block = string.IsNullOrEmpty(block?.Name) ? null : block.Name,
BlockCountry = block?.Country,
BlockStatus = block is { Status: not BlockStatus.Unknown } ? block.Status.ToString() : null,
});
}
static async Task<IResult> Page(HttpContext context, ReleaseFeed feed, CancellationToken ct)
{
var language = Shell.Language(context);
var release = await feed.Latest(ct);
context.Response.Headers.CacheControl = "no-cache";
context.Response.Headers.ContentLanguage = language;
context.Response.Headers.Vary = "Accept-Language";
return Results.Content(DownloadPage.Render(release, language), "text/html; charset=utf-8");
}
static IResult Privacy(HttpContext context)
{
var language = Shell.Language(context);
context.Response.Headers.ContentLanguage = language;
context.Response.Headers.Vary = "Accept-Language";
return Results.Content(PrivacyPage.Render(language), "text/html; charset=utf-8");
}
static async Task<IResult> Download(string platform, ReleaseFeed feed, CancellationToken ct)
{
if (!ReleaseFeed.Platforms.Any(p => p.Platform == platform)) return Results.NotFound();
var asset = ReleaseFeed.Asset(await feed.Latest(ct), platform);
return asset?.Url is null ? Results.Redirect(Endpoints.Gitea.Releases) : Results.Redirect(asset.Url);
}
}