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

152 lines
6.4 KiB
C#
Raw Normal View History

using System.Net.Sockets;
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.Status429TooManyRequests);
if (!speed.Enter()) return Refused(context, new ViewRefusal { Reason = "busy", RetryAfterSeconds = 60 }, StatusCodes.Status429TooManyRequests);
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 ? Results.StatusCode(StatusCodes.Status413PayloadTooLarge) : 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();
var (ticket, refusal, status) = probe.Issue(caller);
return ticket is not null ? Results.Json(ticket, MeWebJsonContext.Default.ViewProbeTicket) : Refused(context, refusal, status);
}
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();
var (result, refusal, status) = await probe.Run(id, caller, form, ct);
return result is not null ? Results.Json(result, MeWebJsonContext.Default.ViewProbe) : Refused(context, refusal, status);
}
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];
int read;
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.Status413PayloadTooLarge);
}
var (shared, refusal, status) = shares.Create(caller, buffer.ToArray());
return shared is not null ? Results.Json(shared, MeWebJsonContext.Default.ViewShared) : Refused(context, refusal, status);
}
static IResult Unshare(string id, HttpContext context, ShareService shares) =>
Results.StatusCode(shares.Delete(id, context.Request.Headers[DeleteTokenHeader].ToString()));
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)
{
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";
return Results.Ok(new ViewIp { Address = address.ToString(), Family = family });
}
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);
}
}