PrivaPub now finds CDNs three ways, best first: the address ranges the CDNs publish (Cloudflare, Fastly, Amazon CloudFront, Bunny, Gcore, Imperva), downloaded daily by CdnUpdater and kept in CdnRangeSet; the CDN's fingerprint in the responses it already gets from a server (EdgeHintsHandler on the federation client); and the networks that carry only a CDN. The fixed ASN list is gone; ASNs shared with plain hosting (AWS, DataPacket) no longer hide a server. A server's Geo records the CDN, its domain and how it was found, and weekly snapshots now keep the city and coordinates too. Servers through time (ServerPlaces): /instances/:host/history lists a server's weekly snapshots, a CDN-fronted server's geo names the CDN's domain and where the server was before it (before_cdn), and /api/privapub/v1/cdns and /cdns/:domain group servers by CDN with week by week who joined and who left. Owner decisions recorded in ROADMAP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
165 lines
5.3 KiB
C#
165 lines
5.3 KiB
C#
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.DataProtection;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.AspNetCore.TestHost;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
|
|
using PrivaPub.Api.Mastodon.Auth;
|
|
using PrivaPub.Domain.Media;
|
|
using PrivaPub.Infrastructure.Jobs;
|
|
using PrivaPub.Infrastructure.Statistics;
|
|
|
|
using System.Net;
|
|
|
|
namespace PrivaPub.Tests.Support.Host
|
|
{
|
|
public class PrivaPubHost : WebApplicationFactory<Program>
|
|
{
|
|
public const string Host = "privapub.test";
|
|
public const string Base = "https://" + Host;
|
|
public const string ClientHeader = "X-Test-Client";
|
|
|
|
static readonly SemaphoreSlim Boot = new(1, 1);
|
|
static readonly Type[] Unwanted = { typeof(JobWorker), typeof(MediaJanitor), typeof(OAuthPruner), typeof(StatisticsSchedule) };
|
|
static PrivaPubHost _shared;
|
|
|
|
readonly string _mediaRoot = Path.Combine(Path.GetTempPath(), $"privapub-tests-{Guid.NewGuid():N}");
|
|
|
|
public IReadOnlyList<ServiceDescriptor> Registered { get; private set; }
|
|
|
|
public static async Task<PrivaPubHost> Shared()
|
|
{
|
|
await Boot.WaitAsync();
|
|
try
|
|
{
|
|
if (_shared == default)
|
|
{
|
|
var host = new PrivaPubHost();
|
|
_ = host.Services;
|
|
_shared = host;
|
|
}
|
|
return _shared;
|
|
}
|
|
finally
|
|
{
|
|
Boot.Release();
|
|
}
|
|
}
|
|
|
|
protected virtual IEnumerable<KeyValuePair<string, string>> Settings() => new Dictionary<string, string>
|
|
{
|
|
["MongoSettings:ConnectionString"] = MongoFixture.Connection,
|
|
["MongoSettings:Database"] = MongoFixture.Database,
|
|
["MongoSettings:LogsDatabase"] = "logs",
|
|
["AppConfiguration:Version"] = "0.0.0-test",
|
|
["AppConfiguration:BackendBaseAddress"] = Base,
|
|
["AppConfiguration:MaxAllowedUploadFiles"] = "3",
|
|
["AppConfiguration:MaxAllowedFileSize"] = "2097152",
|
|
["AppConfiguration:SupportedLanguages:0"] = "en",
|
|
["AppConfiguration:SupportedLanguages:1"] = "it",
|
|
["AppConfiguration:HashingOptions:Iterations"] = "1000",
|
|
["AppConfiguration:Jwt:Key"] = "privapub-tests-only-signing-key-0123456789abcdef0123456789abcdef",
|
|
["AppConfiguration:Jwt:Issuer"] = Base,
|
|
["AppConfiguration:Jwt:Audience"] = Base,
|
|
["AppConfiguration:Jwt:HoursTimeout"] = "1",
|
|
["AppConfiguration:EmailConfiguration:SmtpServer"] = "127.0.0.1",
|
|
["AppConfiguration:EmailConfiguration:SmtpPort"] = "9",
|
|
["AppConfiguration:EmailConfiguration:UseSSL"] = "false",
|
|
["AppConfiguration:EmailConfiguration:SmtpUsername"] = "nobody@privapub.test",
|
|
["AppConfiguration:EmailConfiguration:SmtpPassword"] = "none",
|
|
["Federation:AllowPrivateNetworks"] = "true",
|
|
["Federation:AllowPlainHttp"] = "true",
|
|
["Federation:FetchLinkPreviews"] = "false",
|
|
["Registrations:Mode"] = "Open",
|
|
["Statistics:Geo:AutoUpdate"] = "false",
|
|
["Statistics:Cdn:AutoUpdate"] = "false",
|
|
["Media:Root"] = _mediaRoot,
|
|
["Logging:LogLevel:Default"] = "Warning",
|
|
["Serilog:MinimumLevel:Default"] = Environment.GetEnvironmentVariable("PRIVAPUB_TEST_LOGS") == "1" ? "Information" : "Fatal"
|
|
};
|
|
|
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
|
{
|
|
builder.UseEnvironment("Testing");
|
|
foreach (var (key, value) in Settings())
|
|
builder.UseSetting(key, value);
|
|
builder.ConfigureTestServices(services =>
|
|
{
|
|
foreach (var hosted in services.Where(s => s.ServiceType == typeof(IHostedService) && Unwanted.Contains(s.ImplementationType)).ToList())
|
|
services.Remove(hosted);
|
|
services.AddDataProtection().UseEphemeralDataProtectionProvider();
|
|
services.AddSingleton<IStartupFilter, ClientAddressFilter>();
|
|
Registered = services.ToList();
|
|
});
|
|
}
|
|
|
|
protected override void ConfigureClient(HttpClient client)
|
|
{
|
|
client.BaseAddress = new Uri(Base + "/");
|
|
client.DefaultRequestHeaders.Add(ClientHeader, $"10.{Random.Shared.Next(256)}.{Random.Shared.Next(256)}.{Random.Shared.Next(1, 255)}");
|
|
}
|
|
|
|
public HttpClient Client(bool cookies = false) => CreateClient(new WebApplicationFactoryClientOptions
|
|
{
|
|
BaseAddress = new Uri(Base + "/"),
|
|
AllowAutoRedirect = false,
|
|
HandleCookies = cookies
|
|
});
|
|
|
|
public T Get<T>() where T : notnull => Services.GetRequiredService<T>();
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
base.Dispose(disposing);
|
|
if (disposing && Directory.Exists(_mediaRoot))
|
|
Directory.Delete(_mediaRoot, recursive: true);
|
|
}
|
|
|
|
sealed class ClientAddressFilter : IStartupFilter
|
|
{
|
|
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) => app =>
|
|
{
|
|
app.Use(async (context, call) =>
|
|
{
|
|
if (IPAddress.TryParse(context.Request.Headers[ClientHeader].ToString(), out var address))
|
|
context.Connection.RemoteIpAddress = address;
|
|
await call(context);
|
|
});
|
|
next(app);
|
|
};
|
|
}
|
|
}
|
|
|
|
public sealed class SecureModeHost : PrivaPubHost
|
|
{
|
|
static readonly SemaphoreSlim Boot = new(1, 1);
|
|
static SecureModeHost _shared;
|
|
|
|
public static new async Task<SecureModeHost> Shared()
|
|
{
|
|
await PrivaPubHost.Shared();
|
|
await Boot.WaitAsync();
|
|
try
|
|
{
|
|
if (_shared == default)
|
|
{
|
|
var host = new SecureModeHost();
|
|
_ = host.Services;
|
|
_shared = host;
|
|
}
|
|
return _shared;
|
|
}
|
|
finally
|
|
{
|
|
Boot.Release();
|
|
}
|
|
}
|
|
|
|
protected override IEnumerable<KeyValuePair<string, string>> Settings() =>
|
|
base.Settings().Append(new KeyValuePair<string, string>("Federation:SecureMode", "true"));
|
|
}
|
|
}
|