using System.Collections.Concurrent; using System.Security.Cryptography; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; namespace ClawdDotNet.Manager.Web; /// /// #764: One-time codes that sign the shell's browser window in. /// /// Why not hand the shell the access token. The shell would have to put it into /// the address it opens, and an address ends up in histories, logs and window titles. A code /// that works once and for at most a minute is worth nothing afterwards. The token itself only /// travels in the answer to the exchange, the same way it travels when someone types it in. /// /// Who gets a code: only a request over the manager's local socket /// (), which the file system restricts to the user running the /// manager. Over TCP the endpoint does not exist. /// public sealed class ShellLogin(TimeProvider? clock = null) { public static readonly TimeSpan Lifetime = TimeSpan.FromSeconds(60); private readonly TimeProvider _clock = clock ?? TimeProvider.System; /// Codes by their hash — the code itself is not kept, so a memory dump does not hold usable ones. private readonly ConcurrentDictionary _open = new(); public string Issue() { Sweep(); var code = Base64Url(RandomNumberGenerator.GetBytes(32)); _open[Hash(code)] = _clock.GetUtcNow() + Lifetime; return code; } /// /// True once for a code that was issued and has not expired. The code is gone afterwards /// either way: a second use, or a use after expiry, fails. /// public bool Redeem(string? code) { if (string.IsNullOrWhiteSpace(code) || code.Length > 100) return false; return _open.TryRemove(Hash(code.Trim()), out var expires) && _clock.GetUtcNow() <= expires; } private void Sweep() { var now = _clock.GetUtcNow(); foreach (var (key, expires) in _open) { if (expires < now) _open.TryRemove(key, out _); } } private static string Hash(string code) => Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(code))); private static string Base64Url(byte[] bytes) => Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_'); } /// /// #764: Marks connections that came in over the manager's local socket — the same pattern as /// the instance's LocalTransport: a mark set by the listener, not a guess from a missing /// IP address. /// public sealed class ShellTransport { private static readonly ShellTransport Instance = new(); private ShellTransport() { } /// The only paths the socket serves. Everything else stays behind the token. public const string PathPrefix = "/api/shell"; public static Func Mark => next => connection => { connection.Features.Set(Instance); return next(connection); }; public static bool Applies(HttpContext context) => context.Features.Get() is not null; }