Stufe 2 (#764): Einmal-Anmeldung fuer die Huelle ueber den Manager-Socket
Der Manager hoert zusaetzlich auf einem lokalen Socket (0600 bzw. Pipe nur fuer den eigenen Benutzer). Dort gibt es nur /api/shell/*, ueber TCP gibt es diese Pfade nicht. Ein Einmal-Code (256 Bit, einmal, hoechstens 60 s) wird in der WebUI aus dem Fragment gegen das Token getauscht und sofort aus der Adresse entfernt. Tests: zweimal, zu spaet, ueber TCP - alle scheitern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c3927292fd
commit
775c4dbc8e
@@ -0,0 +1,88 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace ClawdDotNet.Manager.Web;
|
||||
|
||||
/// <summary>
|
||||
/// #764: One-time codes that sign the shell's browser window in.
|
||||
///
|
||||
/// <para><b>Why not hand the shell the access token.</b> 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.</para>
|
||||
///
|
||||
/// <para><b>Who gets a code:</b> only a request over the manager's local socket
|
||||
/// (<see cref="ShellTransport"/>), which the file system restricts to the user running the
|
||||
/// manager. Over TCP the endpoint does not exist.</para>
|
||||
/// </summary>
|
||||
public sealed class ShellLogin(TimeProvider? clock = null)
|
||||
{
|
||||
public static readonly TimeSpan Lifetime = TimeSpan.FromSeconds(60);
|
||||
|
||||
private readonly TimeProvider _clock = clock ?? TimeProvider.System;
|
||||
|
||||
/// <summary>Codes by their hash — the code itself is not kept, so a memory dump does not hold usable ones.</summary>
|
||||
private readonly ConcurrentDictionary<string, DateTimeOffset> _open = new();
|
||||
|
||||
public string Issue()
|
||||
{
|
||||
Sweep();
|
||||
var code = Base64Url(RandomNumberGenerator.GetBytes(32));
|
||||
_open[Hash(code)] = _clock.GetUtcNow() + Lifetime;
|
||||
return code;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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('/', '_');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #764: Marks connections that came in over the manager's local socket — the same pattern as
|
||||
/// the instance's <c>LocalTransport</c>: a mark set by the listener, not a guess from a missing
|
||||
/// IP address.
|
||||
/// </summary>
|
||||
public sealed class ShellTransport
|
||||
{
|
||||
private static readonly ShellTransport Instance = new();
|
||||
|
||||
private ShellTransport() { }
|
||||
|
||||
/// <summary>The only paths the socket serves. Everything else stays behind the token.</summary>
|
||||
public const string PathPrefix = "/api/shell";
|
||||
|
||||
public static Func<ConnectionDelegate, ConnectionDelegate> Mark
|
||||
=> next => connection =>
|
||||
{
|
||||
connection.Features.Set(Instance);
|
||||
return next(connection);
|
||||
};
|
||||
|
||||
public static bool Applies(HttpContext context)
|
||||
=> context.Features.Get<ShellTransport>() is not null;
|
||||
}
|
||||
Reference in New Issue
Block a user