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