Files
TinfoilVibeServer/TinfoilVibeServer/Middleware/BasicAuthMiddleware.cs
T
ecenshu 995e4aa518 Allow for cancelling downloads from filesystem
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
2025-11-07 16:13:48 +10:30

110 lines
3.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text;
using TinfoilVibeServer.Authentication;
namespace TinfoilVibeServer.Middleware;
/// <summary>
/// Minimal BasicAuth 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\"");
}
}