audit, advisories and doctors/setup work

This commit is contained in:
master
2026-01-13 18:53:39 +02:00
parent 9ca7cb183e
commit d7be6ba34b
811 changed files with 54242 additions and 4056 deletions

View File

@@ -1,3 +1,4 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Text.Json;
@@ -11,10 +12,21 @@ namespace StellaOps.Provcache;
/// </summary>
public sealed class HttpChunkFetcher : ILazyEvidenceFetcher, IDisposable
{
/// <summary>
/// Named client for use with IHttpClientFactory.
/// </summary>
public const string HttpClientName = "provcache-lazy-fetch";
private static readonly string[] DefaultSchemes = ["https", "http"];
private readonly HttpClient _httpClient;
private readonly bool _ownsClient;
private readonly ILogger<HttpChunkFetcher> _logger;
private readonly JsonSerializerOptions _jsonOptions;
private readonly LazyFetchHttpOptions _options;
private readonly IReadOnlyList<string> _allowedHosts;
private readonly IReadOnlySet<string> _allowedSchemes;
private readonly bool _allowAllHosts;
/// <inheritdoc />
public string FetcherType => "http";
@@ -23,9 +35,15 @@ public sealed class HttpChunkFetcher : ILazyEvidenceFetcher, IDisposable
/// Creates an HTTP chunk fetcher with the specified base URL.
/// </summary>
/// <param name="baseUrl">The base URL of the Stella API.</param>
/// <param name="httpClientFactory">HTTP client factory.</param>
/// <param name="logger">Logger instance.</param>
public HttpChunkFetcher(Uri baseUrl, ILogger<HttpChunkFetcher> logger)
: this(CreateClient(baseUrl), ownsClient: true, logger)
/// <param name="options">Lazy fetch HTTP options.</param>
public HttpChunkFetcher(
Uri baseUrl,
IHttpClientFactory httpClientFactory,
ILogger<HttpChunkFetcher> logger,
LazyFetchHttpOptions? options = null)
: this(CreateClient(httpClientFactory, baseUrl), ownsClient: false, logger, options)
{
}
@@ -35,25 +53,149 @@ public sealed class HttpChunkFetcher : ILazyEvidenceFetcher, IDisposable
/// <param name="httpClient">The HTTP client to use.</param>
/// <param name="ownsClient">Whether this fetcher owns the client lifecycle.</param>
/// <param name="logger">Logger instance.</param>
public HttpChunkFetcher(HttpClient httpClient, bool ownsClient, ILogger<HttpChunkFetcher> logger)
/// <param name="options">Lazy fetch HTTP options.</param>
public HttpChunkFetcher(
HttpClient httpClient,
bool ownsClient,
ILogger<HttpChunkFetcher> logger,
LazyFetchHttpOptions? options = null)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_ownsClient = ownsClient;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options ?? new LazyFetchHttpOptions();
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
var baseAddress = _httpClient.BaseAddress
?? throw new InvalidOperationException("HttpChunkFetcher requires a BaseAddress on the HTTP client.");
_allowedSchemes = NormalizeSchemes(_options.AllowedSchemes);
_allowedHosts = NormalizeHosts(_options.AllowedHosts, baseAddress.Host, out _allowAllHosts);
ValidateBaseAddress(baseAddress);
ApplyClientConfiguration();
}
private static HttpClient CreateClient(Uri baseUrl)
private static HttpClient CreateClient(IHttpClientFactory httpClientFactory, Uri baseUrl)
{
var client = new HttpClient { BaseAddress = baseUrl };
client.DefaultRequestHeaders.Add("Accept", "application/json");
ArgumentNullException.ThrowIfNull(httpClientFactory);
ArgumentNullException.ThrowIfNull(baseUrl);
var client = httpClientFactory.CreateClient(HttpClientName);
client.BaseAddress = baseUrl;
return client;
}
private void ApplyClientConfiguration()
{
var timeout = _options.Timeout;
if (timeout <= TimeSpan.Zero || timeout == Timeout.InfiniteTimeSpan)
{
throw new InvalidOperationException("Lazy fetch HTTP timeout must be a positive, non-infinite duration.");
}
if (_httpClient.Timeout == Timeout.InfiniteTimeSpan || _httpClient.Timeout > timeout)
{
_httpClient.Timeout = timeout;
}
if (!_httpClient.DefaultRequestHeaders.Accept.Any(header =>
string.Equals(header.MediaType, "application/json", StringComparison.OrdinalIgnoreCase)))
{
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
}
}
private void ValidateBaseAddress(Uri baseAddress)
{
if (!baseAddress.IsAbsoluteUri)
{
throw new InvalidOperationException("Lazy fetch base URL must be absolute.");
}
if (!string.IsNullOrWhiteSpace(baseAddress.UserInfo))
{
throw new InvalidOperationException("Lazy fetch base URL must not include user info.");
}
if (string.IsNullOrWhiteSpace(baseAddress.Host))
{
throw new InvalidOperationException("Lazy fetch base URL must include a host.");
}
if (!_allowedSchemes.Contains(baseAddress.Scheme))
{
throw new InvalidOperationException($"Lazy fetch base URL scheme '{baseAddress.Scheme}' is not allowed.");
}
if (!_allowAllHosts && !IsHostAllowed(baseAddress.Host, _allowedHosts))
{
throw new InvalidOperationException($"Lazy fetch base URL host '{baseAddress.Host}' is not allowlisted.");
}
}
private static IReadOnlySet<string> NormalizeSchemes(IList<string> schemes)
{
var normalized = schemes
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(s => s.Trim())
.ToArray();
if (normalized.Length == 0)
{
normalized = DefaultSchemes;
}
return new HashSet<string>(normalized, StringComparer.OrdinalIgnoreCase);
}
private static IReadOnlyList<string> NormalizeHosts(
IList<string> hosts,
string baseHost,
out bool allowAllHosts)
{
var normalized = hosts
.Where(h => !string.IsNullOrWhiteSpace(h))
.Select(h => h.Trim())
.ToList();
allowAllHosts = normalized.Any(h => string.Equals(h, "*", StringComparison.Ordinal));
if (!allowAllHosts && normalized.Count == 0)
{
normalized.Add(baseHost);
}
return normalized;
}
private static bool IsHostAllowed(string host, IReadOnlyList<string> allowedHosts)
{
foreach (var allowed in allowedHosts)
{
if (string.Equals(allowed, host, StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (allowed.StartsWith("*.", StringComparison.Ordinal))
{
var suffix = allowed[1..];
if (host.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
return false;
}
/// <inheritdoc />
public async Task<FetchedChunk?> FetchChunkAsync(
string proofRoot,

View File

@@ -0,0 +1,33 @@
using System.ComponentModel.DataAnnotations;
namespace StellaOps.Provcache;
/// <summary>
/// Options for HTTP lazy evidence fetching.
/// </summary>
public sealed class LazyFetchHttpOptions
{
/// <summary>
/// Configuration section name under Provcache.
/// </summary>
public const string SectionName = "LazyFetchHttp";
/// <summary>
/// HTTP timeout for fetch requests.
/// Default: 10 seconds.
/// </summary>
[Range(typeof(TimeSpan), "00:00:01", "00:05:00", ErrorMessage = "Timeout must be between 1 second and 5 minutes")]
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
/// <summary>
/// Allowlisted hostnames for HTTP fetches.
/// Supports exact match and "*.example.com" suffix entries.
/// </summary>
public IList<string> AllowedHosts { get; } = new List<string>();
/// <summary>
/// Allowlisted schemes for HTTP fetches.
/// When empty, defaults to http and https.
/// </summary>
public IList<string> AllowedSchemes { get; } = new List<string>();
}