using DodoSSH.Api.Features.Relay; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection.KeyManagement; using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.Extensions.Options; namespace DodoSSH.Api.Setup; /// The relay's services. See ADR 0004. internal static class RelaySetup { /// /// How much longer than the drain the host waits before it stops regardless. /// /// /// Room for the close handshakes and the audit rows' final writes once the drain has run out, which /// is the part of a shutdown an operator would otherwise lose without noticing. /// private static readonly TimeSpan ShutdownMargin = TimeSpan.FromSeconds(10); internal static IServiceCollection AddDodoRelay(this IServiceCollection services) { // Singletons: a session outlives the request that opened it, a protector is safe to share, and // the network seam holds no state. services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // The application name isolates this deployment's payloads from any other ASP.NET application // sharing a key directory, and — unlike the default, which is derived from the content root — // is the same on every node, which is what lets any node verify any node's ticket. services.AddDataProtection().SetApplicationName("DodoSSH"); services.AddOptions() .Configure, ILoggerFactory>((keys, relay, loggers) => { if (relay.Value.KeyRingDirectory is { Length: > 0 } directory) { keys.XmlRepository = new FileSystemXmlRepository(new DirectoryInfo(directory), loggers); } }); // Longer than the drain, or the host would stop waiting for live sessions before the drain ADR 0004 // promises them had run. Harmless with the relay off: nothing else holds a request open that long, // and the host stops as soon as the last request has. services.AddOptions() .Configure>((host, relay) => { var wanted = relay.Value.DrainTimeout + ShutdownMargin; if (wanted > host.ShutdownTimeout) { host.ShutdownTimeout = wanted; } }); return services; } }