Files
MeWeb/src/MeWeb.Server/ServerApi.cs
T
thepraandClaude Opus 5.5 6209c7f2e1
Build / Desktop, server and tools (push) Successful in 1m10s
Release / Every platform, the server, the release (push) Successful in 29m11s
me-web? 0.15.0: address blocks from Asia, Africa and Latin America too
- The provider card now names the address block your connection belongs to in Asia and the Pacific, in Africa, and
  in Latin America and the Caribbean, not only in Europe and the Middle East. The blocks come from the address
  registries APNIC, AFRINIC and LACNIC, used with their permission, and travel inside the app, so this works
  without a connection too.
- In Latin America and the Caribbean the registry gives no names for its blocks, so the card says how the block was
  handed out instead, for example "passed on by the provider to one of its customers".
- The app is about 6 MB larger, because it carries these blocks.
- The app's name no longer has quotation marks where it stands on its own: the window title, the name under the
  icon, the version line and the website's titles. Inside a sentence it keeps them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:32:49 +01:00

162 lines
6.7 KiB
C#

using System.Net.Sockets;
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.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, 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);
}
}