Compare commits
3 Commits
d726e28876
...
48deddad46
| Author | SHA1 | Date | |
|---|---|---|---|
| 48deddad46 | |||
| e34ef136ff | |||
| 45deadc037 |
92
SfeduSchedule/Middleware/LocalNetworksOnlyMiddleware.cs
Normal file
92
SfeduSchedule/Middleware/LocalNetworksOnlyMiddleware.cs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
|
||||||
|
namespace SfeduSchedule.Middleware;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Middleware ограничивает доступ к endpoint'у (сделано для /metrics) только приватными сетями.
|
||||||
|
/// Допускаются: loopback, RFC1918 (10/8, 172.16/12, 192.168/16), link-local (169.254/16, IPv6 link-local),
|
||||||
|
/// а также уникальные локальные адреса IPv6 (fc00::/7). Любой другой источник получает 403.
|
||||||
|
/// Только метод GET.
|
||||||
|
/// </summary>
|
||||||
|
public class LocalNetworksOnlyMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<LocalNetworksOnlyMiddleware> _logger;
|
||||||
|
|
||||||
|
public LocalNetworksOnlyMiddleware(RequestDelegate next, ILogger<LocalNetworksOnlyMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
// Разрешаем только GET
|
||||||
|
if (!HttpMethods.IsGet(context.Request.Method))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Metrics method not allowed: {Method} {Path}", context.Request.Method, context.Request.Path);
|
||||||
|
context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed;
|
||||||
|
context.Response.Headers["Allow"] = "GET";
|
||||||
|
await context.Response.WriteAsync("Method Not Allowed. Only GET is supported for metrics.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ip = context.Connection.RemoteIpAddress;
|
||||||
|
// Если пришёл IPv4, инкапсулированный в IPv6 (::ffff:x.y.z.w), разворачиваем в чистый IPv4.
|
||||||
|
if (ip is { IsIPv4MappedToIPv6: true })
|
||||||
|
{
|
||||||
|
ip = ip.MapToIPv4();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем принадлежность IP локальным/приватным сетям.
|
||||||
|
if (ip is null || !IsLocalNetwork(ip))
|
||||||
|
{
|
||||||
|
// Фиксируем X-Forwarded-For (если есть) для диагностики за обратными прокси.
|
||||||
|
var xff = context.Request.Headers.TryGetValue("X-Forwarded-For", out var xffVal) ? xffVal.ToString() : null;
|
||||||
|
_logger.LogWarning("Metrics access forbidden. RemoteIP={RemoteIP}, XFF={XFF}, Path={Path}", ip?.ToString() ?? "null", xff, context.Request.Path);
|
||||||
|
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||||
|
await context.Response.WriteAsync("Forbidden: metrics available only from local networks");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Продолжаем конвейер, если IP допустим.
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Определяет, принадлежит ли адрес локальным / приватным диапазонам.
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsLocalNetwork(IPAddress ip)
|
||||||
|
{
|
||||||
|
// Loopback (127.0.0.0/8, ::1)
|
||||||
|
if (IPAddress.IsLoopback(ip))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (ip.AddressFamily == AddressFamily.InterNetwork)
|
||||||
|
{
|
||||||
|
var b = ip.GetAddressBytes();
|
||||||
|
// RFC1918: 10.0.0.0/8
|
||||||
|
if (b[0] == 10) return true;
|
||||||
|
// RFC1918: 172.16.0.0 – 172.31.255.255 (172.16/12)
|
||||||
|
if (b[0] == 172 && b[1] >= 16 && b[1] <= 31) return true;
|
||||||
|
// RFC1918: 192.168.0.0/16
|
||||||
|
if (b[0] == 192 && b[1] == 168) return true;
|
||||||
|
// RFC3927: link-local 169.254.0.0/16 (APIPA)
|
||||||
|
if (b[0] == 169 && b[1] == 254) return true; // link-local
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ip.AddressFamily == AddressFamily.InterNetworkV6)
|
||||||
|
{
|
||||||
|
// Link-local (fe80::/10)
|
||||||
|
if (ip.IsIPv6LinkLocal) return true;
|
||||||
|
var b = ip.GetAddressBytes();
|
||||||
|
// ULA (Unique Local Address) RFC4193: fc00::/7 (fc00 – fdff)
|
||||||
|
if (b.Length > 0 && (b[0] == 0xFC || b[0] == 0xFD)) return true; // ULA
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ using Microsoft.AspNetCore.HttpOverrides;
|
|||||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||||
using SfeduSchedule.Auth;
|
using SfeduSchedule.Auth;
|
||||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||||
|
using Prometheus;
|
||||||
|
using SfeduSchedule.Middleware;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -250,10 +252,15 @@ if (string.IsNullOrEmpty(preinstalledJwtToken))
|
|||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseSwaggerUI();
|
app.UseSwaggerUI();
|
||||||
|
|
||||||
|
app.UseStaticFiles();
|
||||||
|
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseHttpMetrics();
|
||||||
|
app.UseRateLimiter();
|
||||||
|
|
||||||
app.MapGet("/", async context =>
|
app.MapGet("/", async context =>
|
||||||
{
|
{
|
||||||
@@ -263,12 +270,18 @@ app.MapGet("/", async context =>
|
|||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
// Ограничим доступ к /metrics только локальными сетями
|
||||||
|
app.UseWhen(ctx => ctx.Request.Path.StartsWithSegments("/metrics", StringComparison.OrdinalIgnoreCase), branch =>
|
||||||
|
{
|
||||||
|
branch.UseMiddleware<LocalNetworksOnlyMiddleware>();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapMetrics();
|
||||||
|
|
||||||
// Маршруты Minimal API из плагинов
|
// Маршруты Minimal API из плагинов
|
||||||
foreach (var p in loadedPlugins)
|
foreach (var p in loadedPlugins)
|
||||||
{
|
{
|
||||||
p.Instance.MapEndpoints(app);
|
p.Instance.MapEndpoints(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseRateLimiter();
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
@@ -10,11 +10,12 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Ical.Net" Version="5.1.0" />
|
<PackageReference Include="Ical.Net" Version="5.1.1" />
|
||||||
<PackageReference Include="Microsoft.Identity.Web" Version="3.14.0" />
|
<PackageReference Include="Microsoft.Identity.Web" Version="3.14.1" />
|
||||||
<PackageReference Include="Microsoft.Playwright" Version="1.55.0" />
|
<PackageReference Include="Microsoft.Playwright" Version="1.55.0" />
|
||||||
<PackageReference Include="Quartz.AspNetCore" Version="3.15.0" />
|
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
<PackageReference Include="Quartz.AspNetCore" Version="3.15.1" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||||
<PackageReference Include="X.Extensions.Logging.Telegram" Version="2.0.2" />
|
<PackageReference Include="X.Extensions.Logging.Telegram" Version="2.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user