995e4aa518
Rebuild request orignally will use setting for constructing the url Rebuild request from client via no-cache will use httppcontext to get runtime pathing to generate url Escape the url generated
110 lines
3.8 KiB
C#
110 lines
3.8 KiB
C#
using System.Text;
|
||
using TinfoilVibeServer.Authentication;
|
||
|
||
namespace TinfoilVibeServer.Middleware;
|
||
|
||
/// <summary>
|
||
/// Minimal Basic‑Auth middleware that also checks UID, failure counters and a blacklist.
|
||
/// </summary>
|
||
public sealed class BasicAuthMiddleware
|
||
{
|
||
private readonly RequestDelegate _next;
|
||
|
||
public BasicAuthMiddleware(RequestDelegate next)
|
||
{
|
||
_next = next;
|
||
}
|
||
|
||
public async Task InvokeAsync(HttpContext context, IAuthStore store, ILogger<BasicAuthMiddleware> logger)
|
||
{
|
||
// ------------- 1) Bypass auth for every path except “/” ----------------
|
||
// PathString is a struct – compare its value directly.
|
||
if (!context.Request.Path.Equals("/", StringComparison.Ordinal))
|
||
{
|
||
await _next(context);
|
||
return;
|
||
}
|
||
|
||
logger.LogInformation("Incoming request from {IP} – {Method} {Path}", context.Connection.RemoteIpAddress, context.Request.Method, context.Request.Path);
|
||
var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||
|
||
// 1) IP blacklist
|
||
if (store.IsIPBlacklisted(ip))
|
||
{
|
||
logger.LogWarning("Blocked request from blacklisted IP {IP}", ip);
|
||
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||
await context.Response.WriteAsync("Forbidden");
|
||
return;
|
||
}
|
||
|
||
// 2) Authorization header
|
||
if (!context.Request.Headers.TryGetValue("Authorization", out var authHeaders))
|
||
{
|
||
logger.LogWarning("Missing Authorization header from {IP}", ip);
|
||
Challenge(context);
|
||
logger.LogInformation("Sent 401 challenge to client");
|
||
return;
|
||
}
|
||
|
||
var authHeader = authHeaders.FirstOrDefault() ?? "";
|
||
if (!authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
Challenge(context);
|
||
logger.LogInformation("Sent 401 challenge to client");
|
||
return;
|
||
}
|
||
|
||
string decoded;
|
||
try
|
||
{
|
||
var b64 = authHeader[6..].Trim();
|
||
decoded = Encoding.UTF8.GetString(Convert.FromBase64String(b64));
|
||
}
|
||
catch
|
||
{
|
||
Challenge(context);
|
||
logger.LogInformation("Sent 401 challenge to client");
|
||
return;
|
||
}
|
||
|
||
var parts = decoded.Split(':', 2);
|
||
if (parts.Length != 2)
|
||
{
|
||
Challenge(context);
|
||
logger.LogInformation("Sent 401 challenge to client");
|
||
return;
|
||
}
|
||
|
||
var username = parts[0];
|
||
var password = parts[1];
|
||
|
||
// 3) UID header (optional)
|
||
int? uid = null;
|
||
if (context.Request.Headers.TryGetValue("UID", out var uidHeader))
|
||
{
|
||
if (int.TryParse(uidHeader.ToString(), out var parsedUid))
|
||
uid = parsedUid;
|
||
}
|
||
|
||
// 4) Validate
|
||
if (!store.TryValidate(username, password, uid, ip, out var error))
|
||
{
|
||
logger.LogWarning("Auth failed for user {User} from {IP}: {Error}", username, ip, error);
|
||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||
context.Response.Headers.Append("WWW-Authenticate", "Basic realm=\"FileSnapshot\"");
|
||
await context.Response.WriteAsync(error ?? "Unauthorized");
|
||
return;
|
||
}
|
||
|
||
// Authentication succeeded – attach username for downstream handlers if needed
|
||
context.Items["User"] = username;
|
||
logger.LogInformation("User {User} authenticated successfully (UID={UID})", username, uid);
|
||
await _next(context);
|
||
}
|
||
|
||
private static void Challenge(HttpContext ctx)
|
||
{
|
||
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||
ctx.Response.Headers.Append("WWW-Authenticate", "Basic realm=\"FileSnapshot\"");
|
||
}
|
||
} |