using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Http;
namespace ClawdDotNet.Manager.Web;
///
/// Der Torwächter der Weboberfläche: an die Daten kommt nur, wer das Token hat.
///
/// Warum der Manager einen eigenen hat und nicht den der Instanz. Die Instanz
/// hört seit Stufe 2 nur noch auf ihrem Socket, und dort ist die Grenze das Dateisystem —
/// ihr Torwächter kennt deshalb eine Ausnahme für lokale Verbindungen. Hier gibt es die
/// nicht: Alles unter /api und /events kommt über TCP herein, und alles davon
/// braucht das Token. Eine Regel weniger ist hier eine Regel, die nicht falsch angewandt
/// werden kann.
///
/// Was ohne Token ausgeliefert wird, ist die leere Hülle — HTML, CSS und
/// JavaScript. Das ist kein Zugeständnis, sondern die einzige Bauform, die funktioniert:
/// Ein Browser, der eine Adresse anspringt, kann keine Kopfzeile mitschicken; wäre die
/// Seite selbst gesperrt, gäbe es keinen Ort, an dem man das Token je eingeben könnte.
/// In den Dateien steht nichts, was jemand nicht wissen dürfte.
///
/// Warum der Vergleich in fester Zeit läuft: Ein gewöhnlicher
/// Zeichenkettenvergleich bricht beim ersten Unterschied ab. Wer die Antwortzeit misst,
/// rät das Token Zeichen für Zeichen statt in einem Rutsch — bei einem Dienst, der
/// Agenten mit Dateizugriff steuert, ist das kein theoretischer Einwand.
///
public sealed class AccessToken(byte[] expected)
{
/// Kürzer als das hier darf ein Token nicht sein.
public const int MinimumLength = 20;
public static AccessToken For(string token)
=> new(Encoding.UTF8.GetBytes(token));
/// Ob dieser Pfad ein Token braucht: alles unter /api und /events.
public static bool NeedsToken(PathString path)
=> path.StartsWithSegments("/api") || path.StartsWithSegments("/events");
///
/// Holt das Token aus der Anfrage.
///
/// Authorization: Bearer … ist der Weg. Ein Cookie wäre bequemer und
/// genau deshalb falsch: Der Browser schickt es bei jeder Anfrage automatisch mit,
/// auch bei einer, die eine fremde Seite ausgelöst hat. Eine Kopfzeile, die
/// JavaScript setzen muss, wird nicht von selbst mitgeschickt — damit ist CSRF hier
/// keine eigene Baustelle.
///
/// Die Ausnahme ist der Ereignisstrom: EventSource im Browser kann keine
/// Kopfzeilen setzen. Dafür — und nur dafür — wird das Token auch aus der
/// Abfragezeichenkette gelesen. Eine bewusst in Kauf genommene Schwäche: Adressen mit
/// Abfragezeichenkette landen in Protokollen. Vertretbar, weil die Weboberfläche in
/// der Vorgabe nur auf Loopback hört; wer sie ins Netz stellt, hat einen Reverse Proxy
/// davor und entscheidet dort, was protokolliert wird.
///
public static string? Read(HttpRequest request)
{
var header = request.Headers.Authorization.ToString();
if (header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
return header["Bearer ".Length..].Trim();
if (request.Path.StartsWithSegments("/events")
&& request.Query.TryGetValue("token", out var fromQuery))
return fromQuery.ToString();
return null;
}
public bool Matches(string? candidate)
{
if (string.IsNullOrEmpty(candidate)) return false;
// FixedTimeEquals gibt bei ungleicher Laenge false zurueck, ohne zu werfen. Dass
// dabei die Laenge durchscheint, ist unbedenklich: Sie ist kein Geheimnis,
// sondern eine feste Zahl. Geheim ist der Inhalt, und der wird in fester Zeit
// verglichen.
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(candidate), expected);
}
}