using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using PrivaPub.Federation.Moderation; using PrivaPub.Infrastructure.Http; using PrivaPub.Infrastructure.Statistics; using PrivaPub.Models.Federation; using System.Collections.Concurrent; namespace PrivaPub.Tests.Support { public sealed class Peer : IAsyncDisposable { readonly WebApplication _app; readonly ConcurrentDictionary _documents = new(); readonly ConcurrentDictionary _answers = new(); readonly ConcurrentDictionary _files = new(); public int Port { get; } public string A => $"http://127.0.0.1:{Port}"; public string B => $"http://localhost:{Port}"; public ConcurrentQueue Requests { get; } = new(); Peer(WebApplication app, int port) { _app = app; Port = port; } public static async Task Start() { var builder = WebApplication.CreateSlimBuilder(); builder.WebHost.UseUrls("http://127.0.0.1:0"); var app = builder.Build(); Peer peer = default; app.Run(async context => { var body = context.Request.ContentLength > 0 || context.Request.Headers.TransferEncoding.Count > 0 ? await new StreamReader(context.Request.Body).ReadToEndAsync() : default; peer.Requests.Enqueue(new(context.Request.Method, context.Request.Path, context.Request.Headers["Signature"].ToString(), body, context.Request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString(), StringComparer.OrdinalIgnoreCase))); var key = context.Request.Path.Value; if (peer._files.TryGetValue(key, out var file)) { await Results.Bytes(file.Bytes, file.ContentType, enableRangeProcessing: true).ExecuteAsync(context); return; } if (peer._answers.TryGetValue(key, out var answer)) { if (answer.Delay > TimeSpan.Zero) await Task.Delay(answer.Delay); context.Response.StatusCode = answer.Status; return; } if (!peer._documents.TryGetValue(key, out var document)) { context.Response.StatusCode = StatusCodes.Status404NotFound; return; } context.Response.ContentType = document.ContentType; await context.Response.WriteAsync(document.Text.Replace("{A}", peer.A).Replace("{B}", peer.B)); }); await app.StartAsync(); peer = new Peer(app, new Uri(app.Urls.First()).Port); return peer; } public void Serve(string path, string json) => _documents[path] = (json, "application/activity+json"); public void ServeText(string path, string text, string contentType) => _documents[path] = (text, contentType); public void ServeFile(string path, byte[] bytes, string contentType) => _files[path] = (bytes, contentType); public void Answer(string path, int status, TimeSpan delay = default) => _answers[path] = (status, delay); public static FederationHttp Http(IMemoryCache cache = default, IDomainBlocks blocks = default, IInteractionLedger ledger = default, IConnectedAddresses connected = default) { var options = new FederationOptions { AllowPrivateNetworks = true, AllowPlainHttp = true }; var services = new ServiceCollection(); services.AddHttpClient(FederationHttp.ClientName) .ConfigurePrimaryHttpMessageHandler(() => SafeHttpHandlerFactory.Create(options, connected)); return new FederationHttp(services.BuildServiceProvider().GetRequiredService(), cache ?? new MemoryCache(new MemoryCacheOptions()), new StaticOptions(options), blocks ?? new NoBlocks(), NullLogger.Instance, ledger); } public async ValueTask DisposeAsync() => await _app.DisposeAsync(); } public sealed class NoBlocks : IDomainBlocks { public DomainBlock Find(string host) => default; public bool IsSuspended(string host) => false; public Task Reload(CancellationToken token) => Task.CompletedTask; } public sealed record HttpRequestRecord(string Method, string Path, string Signature, string Body = default, IReadOnlyDictionary Headers = default); public sealed class StaticOptions : IOptionsMonitor { public StaticOptions(T value) => CurrentValue = value; public T CurrentValue { get; } public T Get(string name) => CurrentValue; public IDisposable OnChange(Action listener) => default; } }