Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
console-runner-image / build-runner-image (push) Has been cancelled
wine-csp-build / Build Wine CSP Image (push) Has been cancelled
wine-csp-build / Integration Tests (push) Has been cancelled
wine-csp-build / Security Scan (push) Has been cancelled
wine-csp-build / Generate SBOM (push) Has been cancelled
wine-csp-build / Publish Image (push) Has been cancelled
wine-csp-build / Air-Gap Bundle (push) Has been cancelled
wine-csp-build / Test Summary (push) Has been cancelled
- Added BerkeleyDbReader class to read and extract RPM header blobs from BerkeleyDB hash databases. - Implemented methods to detect BerkeleyDB format and extract values, including handling of page sizes and magic numbers. - Added tests for BerkeleyDbReader to ensure correct functionality and header extraction. feat: Add Yarn PnP data tests - Created YarnPnpDataTests to validate package resolution and data loading from Yarn PnP cache. - Implemented tests for resolved keys, package presence, and loading from cache structure. test: Add egg-info package fixtures for Python tests - Created egg-info package fixtures for testing Python analyzers. - Included PKG-INFO, entry_points.txt, and installed-files.txt for comprehensive coverage. test: Enhance RPM database reader tests - Added tests for RpmDatabaseReader to validate fallback to legacy packages when SQLite is missing. - Implemented helper methods to create legacy package files and RPM headers for testing. test: Implement dual signing tests - Added DualSignTests to validate secondary signature addition when configured. - Created stub implementations for crypto providers and key resolvers to facilitate testing. chore: Update CI script for Playwright Chromium installation - Modified ci-console-exports.sh to ensure deterministic Chromium binary installation for console exports tests. - Added checks for Windows compatibility and environment variable setups for Playwright browsers.
209 lines
7.4 KiB
C#
209 lines
7.4 KiB
C#
using System.Globalization;
|
|
using System.IO;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.Concelier.WebService.Diagnostics;
|
|
using StellaOps.Concelier.WebService.Options;
|
|
using StellaOps.Concelier.WebService.Services;
|
|
using StellaOps.Concelier.WebService.Results;
|
|
using HttpResults = Microsoft.AspNetCore.Http.Results;
|
|
|
|
namespace StellaOps.Concelier.WebService.Extensions;
|
|
|
|
internal static class MirrorEndpointExtensions
|
|
{
|
|
private const string IndexScope = "index";
|
|
private const string DownloadScope = "download";
|
|
|
|
public static void MapConcelierMirrorEndpoints(this WebApplication app, bool authorityConfigured, bool enforceAuthority)
|
|
{
|
|
app.MapGet("/concelier/exports/index.json", async (
|
|
MirrorFileLocator locator,
|
|
MirrorRateLimiter limiter,
|
|
IOptionsMonitor<ConcelierOptions> optionsMonitor,
|
|
HttpContext context,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var mirrorOptions = optionsMonitor.CurrentValue.Mirror ?? new ConcelierOptions.MirrorOptions();
|
|
if (!mirrorOptions.Enabled)
|
|
{
|
|
return ConcelierProblemResultFactory.MirrorNotFound(context);
|
|
}
|
|
|
|
if (!TryAuthorize(mirrorOptions.RequireAuthentication, enforceAuthority, context, authorityConfigured, out var unauthorizedResult))
|
|
{
|
|
return unauthorizedResult;
|
|
}
|
|
|
|
if (!limiter.TryAcquire("__index__", IndexScope, mirrorOptions.MaxIndexRequestsPerHour, out var retryAfter))
|
|
{
|
|
ApplyRetryAfter(context.Response, retryAfter);
|
|
return ConcelierProblemResultFactory.RateLimitExceeded(context, (int?)retryAfter?.TotalSeconds);
|
|
}
|
|
|
|
if (!locator.TryResolveIndex(out var path, out _))
|
|
{
|
|
return ConcelierProblemResultFactory.MirrorNotFound(context);
|
|
}
|
|
|
|
return await WriteFileAsync(context, path, "application/json").ConfigureAwait(false);
|
|
});
|
|
|
|
app.MapGet("/concelier/exports/{**relativePath}", async (
|
|
string? relativePath,
|
|
MirrorFileLocator locator,
|
|
MirrorRateLimiter limiter,
|
|
IOptionsMonitor<ConcelierOptions> optionsMonitor,
|
|
HttpContext context,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
var mirrorOptions = optionsMonitor.CurrentValue.Mirror ?? new ConcelierOptions.MirrorOptions();
|
|
if (!mirrorOptions.Enabled)
|
|
{
|
|
return ConcelierProblemResultFactory.MirrorNotFound(context);
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(relativePath))
|
|
{
|
|
return ConcelierProblemResultFactory.MirrorNotFound(context);
|
|
}
|
|
|
|
if (!locator.TryResolveRelativePath(relativePath, out var path, out _, out var domainId))
|
|
{
|
|
return ConcelierProblemResultFactory.MirrorNotFound(context, relativePath);
|
|
}
|
|
|
|
var domain = FindDomain(mirrorOptions, domainId);
|
|
|
|
if (!TryAuthorize(domain?.RequireAuthentication ?? mirrorOptions.RequireAuthentication, enforceAuthority, context, authorityConfigured, out var unauthorizedResult))
|
|
{
|
|
return unauthorizedResult;
|
|
}
|
|
|
|
var limit = domain?.MaxDownloadRequestsPerHour ?? mirrorOptions.MaxIndexRequestsPerHour;
|
|
if (!limiter.TryAcquire(domain?.Id ?? "__mirror__", DownloadScope, limit, out var retryAfter))
|
|
{
|
|
ApplyRetryAfter(context.Response, retryAfter);
|
|
return ConcelierProblemResultFactory.RateLimitExceeded(context, (int?)retryAfter?.TotalSeconds);
|
|
}
|
|
|
|
var contentType = ResolveContentType(path);
|
|
return await WriteFileAsync(context, path, contentType).ConfigureAwait(false);
|
|
});
|
|
}
|
|
|
|
private static ConcelierOptions.MirrorDomainOptions? FindDomain(ConcelierOptions.MirrorOptions mirrorOptions, string? domainId)
|
|
{
|
|
if (domainId is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
foreach (var candidate in mirrorOptions.Domains)
|
|
{
|
|
if (candidate is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (string.Equals(candidate.Id, domainId, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static bool TryAuthorize(bool requireAuthentication, bool enforceAuthority, HttpContext context, bool authorityConfigured, out IResult result)
|
|
{
|
|
result = HttpResults.Empty;
|
|
if (!requireAuthentication)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!enforceAuthority || !authorityConfigured)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (context.User?.Identity?.IsAuthenticated == true)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
context.Response.Headers.WWWAuthenticate = "Bearer realm=\"StellaOps Concelier Mirror\"";
|
|
result = HttpResults.StatusCode(StatusCodes.Status401Unauthorized);
|
|
return false;
|
|
}
|
|
|
|
private static Task<IResult> WriteFileAsync(HttpContext context, string path, string contentType)
|
|
{
|
|
var fileInfo = new FileInfo(path);
|
|
if (!fileInfo.Exists)
|
|
{
|
|
return Task.FromResult(ConcelierProblemResultFactory.MirrorNotFound(context, path));
|
|
}
|
|
|
|
var stream = new FileStream(
|
|
path,
|
|
FileMode.Open,
|
|
FileAccess.Read,
|
|
FileShare.Read | FileShare.Delete);
|
|
|
|
context.Response.Headers.CacheControl = BuildCacheControlHeader(path);
|
|
context.Response.Headers.LastModified = fileInfo.LastWriteTimeUtc.ToString("R", CultureInfo.InvariantCulture);
|
|
context.Response.ContentLength = fileInfo.Length;
|
|
return Task.FromResult(HttpResults.Stream(stream, contentType));
|
|
}
|
|
|
|
private static string ResolveContentType(string path)
|
|
{
|
|
if (path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return "application/json";
|
|
}
|
|
|
|
if (path.EndsWith(".jws", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return "application/jose+json";
|
|
}
|
|
|
|
return "application/octet-stream";
|
|
}
|
|
|
|
private static void ApplyRetryAfter(HttpResponse response, TimeSpan? retryAfter)
|
|
{
|
|
if (retryAfter is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var seconds = Math.Max((int)Math.Ceiling(retryAfter.Value.TotalSeconds), 1);
|
|
response.Headers.RetryAfter = seconds.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
private static string BuildCacheControlHeader(string path)
|
|
{
|
|
var fileName = Path.GetFileName(path);
|
|
if (fileName is null)
|
|
{
|
|
return "public, max-age=60";
|
|
}
|
|
|
|
if (string.Equals(fileName, "index.json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return "public, max-age=60";
|
|
}
|
|
|
|
if (fileName.EndsWith(".json", StringComparison.OrdinalIgnoreCase) ||
|
|
fileName.EndsWith(".jws", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return "public, max-age=300, immutable";
|
|
}
|
|
|
|
return "public, max-age=300";
|
|
}
|
|
}
|