using Carter; using MediatR; using Knot.Shared.Kernel.Constants; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using System; using Host.Application.Federation.Commands; using Knot.Modules.Federation.Application.Abstractions; using Knot.Shared.Kernel.Storage; using Knot.Shared.Kernel.Configuration; namespace Host.Endpoints; /// /// Эндпоинты федерации для межсерверного взаимодействия. /// public sealed class FederationEndpoints : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { var group = app.MapGroup(Routes.ApiFederation); group.MapPost("/handshake", async ([FromBody] HandshakeRequest request, ISender sender, CancellationToken ct) => { var result = await sender.Send(new HandshakeFederationCommand(request), ct); return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(new { error = result.Error.Description }); }); group.MapPost("/inbound", async ([FromBody] FederationMessagePacket packet, ISender sender, CancellationToken ct) => { var result = await sender.Send(new InboundFederationCommand(packet), ct); return result.IsSuccess ? Results.Accepted() : Results.BadRequest(new { error = result.Error.Description }); }); group.MapGet("/resolve/{username}", async (string username, ISender sender, CancellationToken ct) => { var result = await sender.Send(new ResolveUserCommand(username), ct); return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(new { error = result.Error.Description }); }); group.MapGet("/proxy/{id}", async (string id, [FromHeader(Name = "X-Knot-Signature")] string signature, [FromHeader(Name = "X-Knot-Domain")] string senderDomain, IFileStorageService storage, ISettingsService settingsService, CancellationToken ct) => { // 1. Валидация подписи (упрощенно) var settings = await settingsService.GetSettingsAsync(ct); var senderConfig = settings.Federation.AllowedDomains.FirstOrDefault(d => d.Domain.Equals(senderDomain, StringComparison.OrdinalIgnoreCase)); if (senderConfig == null || string.IsNullOrEmpty(senderConfig.PublicKey)) return Results.Forbid(); // В реальном коде здесь проверка RSA подписи параметров запроса (URL + Domain) // 2. Стриминг файла первоисточника try { var (stream, contentType, fileName) = await storage.DownloadFileAsync(id); return Results.File(stream, contentType, fileName, enableRangeProcessing: true); } catch { return Results.NotFound(); } }); } }