Files
SocialPub/PrivaPub/Program.cs
T
thepraandClaude Opus 5.5 70d1ffe9a2
Build / Build (push) Successful in 1m3s
R5 the server's own settings: instance, rules, announcements, relays, media limits, the staff's log
A singleton ServerSettings (title, descriptions, contact, thumbnail, privacy policy, terms, rules, relays, media
overrides, PublishActivity), cached by InstanceSettings and read by the instance API, NodeInfo, the instance actor, the
web pages and the mailer; its defaults are the old literals, so nothing changes until an administrator edits.
/clientapi/admin/settings (and /thumbnail), /announcements, /relays for administrators; /clientapi/moderator/log, the
staff's log (AdminAction, written by domain blocks, backups, settings and the CLI; no IP, never a root paired with its
personas). The Mastodon API's rules, extended description, privacy policy, terms of service, domain blocks (Published,
Obfuscate), announcements (dismiss, reactions, streaming) and activity (only when published). Reports carry rule ids.
Media overrides clamped to hard caps. Relays named by the settings reconcile next to the configured ones.

Verified: interop.sh instance accounts relay with mastodon, relay, aoderelay and mailpit, 119 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
2026-10-17 04:02:47 +02:00

253 lines
9.1 KiB
C#

using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver;
using MongoDB.Entities;
using Serilog;
using PrivaPub.Data;
using PrivaPub.Extensions;
using PrivaPub.Api.Mastodon.Auth;
using PrivaPub.Api.Mastodon.Infrastructure;
using PrivaPub.Infrastructure;
using PrivaPub.Infrastructure.Cli;
using PrivaPub.Infrastructure.Data;
using PrivaPub.Infrastructure.Http;
using PrivaPub.Infrastructure.Statistics;
using PrivaPub.Middleware;
using PrivaPub.Models;
using PrivaPub.Services;
using PrivaPub.StaticServices;
Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateBootstrapLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(serverOptions =>
{
if (builder.Environment.IsProduction())
{
serverOptions.ListenLocalhost(6970);
serverOptions.UseSystemd();
serverOptions.AddServerHeader = false;
}
});
builder.Host.UseSerilog((context, config) =>
{
config.ReadFrom.Configuration(context.Configuration);
});
try
{
builder.Services.PrivaPubAppSettingsConfiguration(builder.Configuration)
.PrivaPubAuthServicesConfiguration(builder.Configuration)
.PrivaPubInternalizationConfiguration(builder.Configuration)
.PrivaPubOptimizationConfiguration()
.PrivaPubDataBaseConfiguration()
.PrivaPubServicesConfiguration()
.PrivaPubFederationConfiguration(builder.Configuration)
.PrivaPubStatisticsConfiguration(builder.Configuration)
.PrivaPubCORSConfiguration()
.PrivaPubRateLimiting(builder.Configuration)
.PrivaPubOAuth(builder.Environment)
.AddScoped<PrivaPub.Api.Mastodon.Mappers.MastodonMapper>()
.AddScoped<PrivaPub.Api.Mastodon.Mappers.AccountSearch>()
.AddSingleton<PrivaPub.Domain.Social.Trends>()
.Configure<PrivaPub.Domain.Media.MediaOptions>(builder.Configuration.GetSection("Media"))
// the server's own settings (R5): read at start, saved from the administrator's page, read again every five
// minutes; the media limits it changes reach every IOptionsMonitor<MediaOptions> reader
.AddSingleton<PrivaPub.Domain.Instance.IInstanceSettings, PrivaPub.Domain.Instance.InstanceSettings>()
.AddHostedService<PrivaPub.Domain.Instance.InstanceSettingsRefresher>()
.AddSingleton<IPostConfigureOptions<PrivaPub.Domain.Media.MediaOptions>, PrivaPub.Domain.Instance.MediaLimitsFromSettings>()
.AddSingleton<IOptionsChangeTokenSource<PrivaPub.Domain.Media.MediaOptions>, PrivaPub.Domain.Instance.MediaLimitsChangeSource>()
.AddSingleton<PrivaPub.Domain.Moderation.IModerationLog, PrivaPub.Domain.Moderation.ModerationLog>()
.AddSingleton<PrivaPub.Domain.Social.IAnnouncements, PrivaPub.Domain.Social.Announcements>()
.AddSingleton<PrivaPub.Domain.Media.IMediaService, PrivaPub.Domain.Media.MediaService>()
.AddSingleton<PrivaPub.Domain.Media.IMediaProxy, PrivaPub.Domain.Media.MediaProxy>()
.AddHostedService<PrivaPub.Domain.Media.MediaJanitor>()
.Configure<PrivaPub.Infrastructure.Backup.BackupOptions>(builder.Configuration.GetSection("Backups"))
.AddSingleton<PrivaPub.Infrastructure.Backup.Backups>()
.AddSingleton<PrivaPub.Infrastructure.Backup.TransferStore>()
.AddHostedService<PrivaPub.Infrastructure.Backup.BackupScheduler>()
.AddHostedService<PrivaPub.Infrastructure.Backup.RestoreWatcher>()
.PrivaPubMiddlewareConfiguration();
}
catch (Exception ex)
{
Log.ForContext<Program>().Fatal(ex, "{0}.{1}()", nameof(Program), "ConfigureServices");
throw;
}
var federationOptions = builder.Configuration.GetSection("Federation").Get<FederationOptions>() ?? new();
if (builder.Environment.IsProduction() && (federationOptions.AllowPrivateNetworks || federationOptions.AllowPlainHttp || federationOptions.AcceptAnyCertificate))
throw new InvalidOperationException("Federation:AllowPrivateNetworks, AllowPlainHttp and AcceptAnyCertificate are for test networks and must stay off in Production.");
try
{
BsonSerializer.TryRegisterSerializer(new GuidSerializer(GuidRepresentation.Standard));
var mongoSettings = builder.Configuration.GetSection(nameof(MongoSettings)).Get<MongoSettings>();
await DB.InitAsync(mongoSettings.Database, MongoClientSettings.FromConnectionString(mongoSettings.ConnectionString));
EntityMaps.Warm();
}
catch (Exception ex)
{
Log.ForContext<Program>().Fatal(ex, $"{nameof(Program)}.{nameof(Program)}() DB Instantiation");
throw;
}
var app = default(WebApplication);
try
{
app = builder.Build();
}
catch (Exception ex)
{
Log.ForContext<Program>().Fatal(ex, "{0}.{1}()", nameof(Program), "Build");
throw;
}
// A restore asked for runs here, before migrations, indexes and every hosted service; a command waits for it.
var gate = await PrivaPub.Infrastructure.Backup.MaintenanceGate.Enter(app.Services, args is ["admin", .. var asked] ? asked : default, CancellationToken.None);
if (gate is { } stop)
{
Environment.ExitCode = stop;
return;
}
// Commands get every service but start nothing: no Kestrel, no hosted services, no media directory. The deploy runs
// them as its own user, which can read the configuration and reach the private mongod but owns no www-data directory.
// Backups and restores are asked for before migrations: the deploy's backup is of the database as the live build left it.
if (args is ["admin", .. var command] && AdminCommands.BeforeMigrations(command))
{
using var scope = app.Services.CreateScope();
Environment.ExitCode = await AdminCommands.Run(command, scope.ServiceProvider);
return;
}
try
{
await DB.Default.MigrateAsync<Program>();
await Indexes.Create();
await app.Services.GetRequiredService<PrivaPub.Domain.Instance.IInstanceSettings>().Reload(CancellationToken.None);
}
catch (Exception ex)
{
Log.ForContext<Program>().Fatal(ex, $"{nameof(Program)}.{nameof(Program)}() DB Migrations");
throw;
}
if (args is ["admin", ..])
{
using var scope = app.Services.CreateScope();
Environment.ExitCode = await AdminCommands.Run(args[1..], scope.ServiceProvider);
return;
}
try
{
var localizationService = app.Services.GetService<RequestLocalizationOptionsService>();
if (app.Environment.IsProduction())
{
app.UseResponseCompression();
app.UseForwardedHeaders(new()
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
}
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseCors("DefaultCORS");
app.UseMastodonErrorBodies();
app.UseStaticFiles();
var mediaRoot = app.Services.GetRequiredService<PrivaPub.Domain.Media.IMediaService>().Root;
Directory.CreateDirectory(mediaRoot);
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(mediaRoot),
RequestPath = "/media/files",
ContentTypeProvider = PrivaPub.Domain.Media.MediaService.ContentTypes,
OnPrepareResponse = context =>
{
context.Context.Response.Headers["X-Content-Type-Options"] = "nosniff";
context.Context.Response.Headers["Content-Security-Policy"] = "default-src 'none'; sandbox";
context.Context.Response.Headers["Cache-Control"] = "public, max-age=31536000, immutable";
}
});
app.UseRequestLocalization(await localizationService.Get());
app.UseWebSockets(new WebSocketOptions { KeepAliveInterval = TimeSpan.FromSeconds(30) });
app.UseRouting();
app.UseTrafficMeter();
app.UseRateLimiter();
// a stream's token may come as access_token or as the WebSocket's protocol, as Mastodon's clients send it
app.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/api/v1/streaming") && !context.Request.Headers.ContainsKey("Authorization"))
{
var token = context.Request.Query["access_token"].ToString();
if (string.IsNullOrEmpty(token))
token = context.Request.Headers["Sec-WebSocket-Protocol"].ToString();
if (!string.IsNullOrEmpty(token))
context.Request.Headers.Authorization = "Bearer " + token;
}
await next();
});
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/build.json", () => Results.Json(new
{
commit = BuildInfo.Commit,
buildRef = BuildInfo.Ref,
builtAt = BuildInfo.BuiltAt,
}));
app.MapControllers();
app.MapRazorPages();
}
catch (Exception ex)
{
Log.ForContext<Program>().Fatal(ex, "{0}.{1}()", nameof(Program), "Use");
throw;
}
Log.ForContext<Program>().Information($"Starting PrivaPub at {nameof(Program)}()");
try
{
var dbClient = app.Services.GetService(typeof(DbEntities)) as DbEntities;
await dbClient.Init();
await app.Services.GetRequiredService<PrivaPub.Federation.Moderation.IDomainBlocks>().Reload(CancellationToken.None);
await PrivaPub.Api.Mastodon.Auth.PersonaExchange.EnsureFirstPartyClient(app.Services, CancellationToken.None);
}
catch (Exception ex)
{
Log.ForContext<Program>().Warning(ex, $"{nameof(Program)}.{nameof(Program)}() DB Init");
}
await app.RunAsync();
}
catch (Exception ex)
{
Log.ForContext<Program>().Fatal(ex, $"{nameof(Program)}.{nameof(Program)}()");
Environment.ExitCode = 1;
}
finally
{
await Log.CloseAndFlushAsync();
}