Blog/Program.cs 4.5 K · 111 lines · raw · history

1 using Blog.Components;
2 using Blog.Services;
3 using Microsoft.AspNetCore.Mvc;
4
5 var builder = WebApplication.CreateBuilder(args);
6
7 // Add services to the container.
8 builder.Services.AddRazorComponents();
9
10 builder.Services.AddHttpClient();
11 builder.Services.AddHybridCache();
12 builder.Services.AddHttpClient<BrpService>(
13 client =>
14 {
15 client.BaseAddress = new Uri("https://brp.bes.is/");
16 });
17
18 // /Warframe searches Digital Extremes' published drop tables. warframe.com blocks requests from
19 // the server, so the 4 MB page ships in Blog/Resources and scripts/update-droptables.sh refreshes
20 // it before a publish. Singleton: the parsed tables live in the service, so a transient one would
21 // re-parse per request - see WarframeDropService.
22 builder.Services.Configure<WarframeOptions>(builder.Configuration.GetSection(WarframeOptions.Section));
23 builder.Services.AddSingleton<WarframeDropService>();
24
25 // /git browses the bare repositories on this server, in place of the cgit that used to. The
26 // service holds nothing: every read opens the repository it needs and closes it again, so a push
27 // shows up with nothing to invalidate - see GitService.
28 builder.Services.Configure<GitOptions>(builder.Configuration.GetSection(GitOptions.Section));
29 builder.Services.AddSingleton<GitService>();
30
31 // /Send introduces two browsers to each other and then gets out of the way: the signalling
32 // socket below carries their WebRTC handshake, and the file goes straight from one to the other.
33 // Singleton because the rooms are the service - see SignalingService.
34 builder.Services.Configure<SendOptions>(builder.Configuration.GetSection(SendOptions.Section));
35 builder.Services.AddSingleton<SignalingService>();
36
37 // /rvrb reads the rvrb bot's stats straight off its BEAM, over Erlang distribution. The node this
38 // site dials with is started on the first request, not here - see RvrbService.
39 builder.Services.Configure<RvrbOptions>(builder.Configuration.GetSection(RvrbOptions.Section));
40 builder.Services.AddSingleton<RvrbService>();
41
42 // builder.Services.AddTransient<BrpService>();
43 // builder.Services.AddTransient<NsService>();
44
45 var app = builder.Build();
46
47 // Configure the HTTP request pipeline.
48 if (!app.Environment.IsDevelopment())
49 {
50 app.UseExceptionHandler("/Error", createScopeForErrors: true);
51 // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
52 app.UseHsts();
53 }
54
55 app.UseHttpsRedirection();
56
57 // /Send's signalling socket. Nothing else on this site uses WebSockets.
58 app.UseWebSockets();
59
60 app.UseAntiforgery();
61
62 app.MapStaticAssets();
63 app.MapRazorComponents<App>();
64
65 // One file out of a repository, exactly as it was committed.
66 //
67 // Never inline HTML: these repositories hold .html and .svg files, and serving one from this
68 // origin would run whatever a commit put in it as a page of mb.bes.is. Text goes out as
69 // text/plain with nosniff, which a browser will not reinterpret, and everything else is a
70 // download.
71 app.MapGet("/git/{repository}/raw/{**path}", (
72 string repository,
73 string path,
74 [FromQuery(Name = "h")] string? reference,
75 GitService git,
76 HttpResponse response) =>
77 {
78 if (git.GetRawBlob(repository, reference, path) is not { } blob) return Results.NotFound();
79
80 response.Headers.XContentTypeOptions = "nosniff";
81
82 return blob.IsBinary
83 ? Results.File(blob.Bytes, "application/octet-stream", blob.Name)
84 : Results.File(blob.Bytes, "text/plain; charset=utf-8");
85 });
86
87 // Item names matching what has been typed, for the search box's suggestion list.
88 app.MapGet("/api/warframe/names", async (string? q, WarframeDropService drops, CancellationToken cancellationToken) =>
89 {
90 var status = await drops.GetTablesAsync(cancellationToken).ConfigureAwait(false);
91 return status.Tables?.Search(q ?? "", 10) ?? [];
92 });
93
94 // The introduction for /Send: two browsers holding the same room code trade their WebRTC offer,
95 // answer and ICE candidates through here, and once the peer connection is up this socket has
96 // nothing left to carry. The file never passes through this process.
97 app.MapGet("/api/send/{room}", async (
98 string room,
99 HttpContext context,
100 SignalingService signaling) =>
101 {
102 if (!context.WebSockets.IsWebSocketRequest) return Results.BadRequest("Expected a WebSocket request.");
103 if (!SignalingService.IsRoomCode(room)) return Results.BadRequest("Malformed room code.");
104
105 using var socket = await context.WebSockets.AcceptWebSocketAsync();
106 await signaling.RelayAsync(room, socket, context.RequestAborted);
107
108 return Results.Empty;
109 });
110
111 app.Run();