DataTransfer/DataTransfer/Program.cs
2026-10-15 20:09:11 +02:00

122 lines
No EOL
4.9 KiB
C#

using System.Threading.RateLimiting;
using DataTransfer.Client.Shared;
using DataTransfer.Components;
using DataTransfer.Hubs;
using DataTransfer.Security;
using DataTransfer.Services;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Options;
using _Imports = DataTransfer.Client._Imports;
var builder = WebApplication.CreateBuilder(args);
// Kestrel only reads endpoints from its configuration section; bind the connection limits explicitly. They cap the
// sockets (and receive buffers) the server keeps open, whatever the per-address limits let through.
builder.WebHost.ConfigureKestrel(options => builder.Configuration.GetSection("Kestrel:Limits").Bind(options.Limits));
builder.Services.AddRazorComponents()
.AddInteractiveWebAssemblyComponents();
builder.Services.AddSingleton<InvocationRateLimitFilter>();
builder.Services.AddSignalR()
.AddHubOptions<TransferHub>(options =>
{
options.MaximumReceiveMessageSize = TransferProtocol.MaxHubMessageBytes;
options.AddFilter<InvocationRateLimitFilter>();
});
builder.Services.AddOptions<TransferOptions>()
.Bind(builder.Configuration.GetSection(TransferOptions.SectionName))
.Validate(options => options.IsValid(), "Transfer options must contain positive time spans and limits and a non-negative queue length.")
.ValidateOnStart();
builder.Services.AddOptions<LegalOptions>()
.Bind(builder.Configuration.GetSection(LegalOptions.SectionName));
builder.Services.AddOptions<BotCheckOptions>()
.Bind(builder.Configuration.GetSection(BotCheckOptions.SectionName))
.Validate(options => options.IsValid(), "Bot check options must contain positive limits, a busy threshold between 0 and 1 and a positive challenge lifetime.")
.ValidateOnStart();
builder.AddConfiguredForwardedHeaders();
builder.Services.AddRateLimiter(_ => { });
builder.Services.AddOptions<RateLimiterOptions>()
.Configure<IOptions<TransferOptions>>((limiter, transfer) =>
{
var attemptsPerMinute = transfer.Value.MaxConnectAttemptsPerMinute;
limiter.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
limiter.OnRejected = (context, cancellationToken) =>
{
// A content type keeps the status code pages from replacing the response.
context.HttpContext.Response.ContentType = "text/plain";
return new ValueTask(
context.HttpContext.Response.WriteAsync("Too many connection attempts. Please try again later.", cancellationToken));
};
// Browsers start every hub connection with a negotiate request, so this gives them a clear HTTP 429. The hub
// enforces the same limit again for clients that skip the negotiate request.
limiter.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
context.Request.Path.StartsWithSegments($"{TransferProtocol.HubPath}/negotiate")
? RateLimitPartition.GetFixedWindowLimiter(
ClientAddress.GetPartitionKey(context.Connection.RemoteIpAddress),
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = attemptsPerMinute,
Window = TimeSpan.FromMinutes(1)
})
: RateLimitPartition.GetNoLimiter(string.Empty));
});
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<ClientAddressLimiter>();
builder.Services.AddSingleton<BotCheck>();
builder.Services.AddSingleton<ITransferService, TransferService>();
builder.Services.AddHostedService<ConnectionCleanupService>();
var app = builder.Build();
if (!app.Environment.IsDevelopment() && app.Services.GetRequiredService<IOptions<LegalOptions>>().Value.HasPlaceholders())
{
app.Logger.LogWarning(
"The imprint, privacy policy and terms of use still contain placeholders. Fill in the {Section} configuration section before going live.",
LegalOptions.SectionName);
}
app.UseConfiguredForwardedHeaders();
if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error", true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseSecurityHeaders(app.Environment.IsDevelopment());
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
app.UseHttpsRedirection();
app.UseHubNegotiateGuard();
app.UseRateLimiter();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapHub<TransferHub>(TransferProtocol.HubPath);
app.MapGet(
TransferProtocol.StatusPath,
(HttpContext context, ITransferService transferService) =>
{
context.Response.Headers.CacheControl = "no-store";
return transferService.GetStatus();
});
app.MapRazorComponents<App>()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(_Imports).Assembly);
app.Run();