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;
}