122 lines
No EOL
4.9 KiB
C#
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(); |