using System.Reflection; using DodoSSH.Api.Setup; using DodoSSH.Contracts; using DodoSSH.Crypto; using FastEndpoints; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Extensions.Options; namespace DodoSSH.Api.Features.Meta; /// /// Capability and discovery endpoints. /// /// /// These replace URL-based API versioning. When client and server upgrade independently — which is /// the normal case for self-hosted software — a client needs to ask what this particular server /// supports rather than assume. See ADR 0002. /// internal sealed class GetMetaEndpoint( IOptions sync, IOptions relay, IOptions events, IOptions server) : EndpointWithoutRequest> { /// Sync semantics version. Bumped when push or pull behaviour changes. internal const int SyncProtocolVersion = 1; /// public override void Configure() { Get("/api/v1/meta"); // Anonymous by necessity: a client must be able to discover how to authenticate before it // can authenticate. Load-bearing rather than decorative — FastEndpoints authenticates every // endpoint that does not say otherwise, so removing this line is a 401 nobody can recover from. AllowAnonymous(); Description(b => b .WithName("GetMeta") .WithSummary("Server capabilities, versions and limits.")); } /// public override Task> ExecuteAsync(CancellationToken ct) { List features = ["teams"]; if (relay.Value.Enabled) { features.Add(RelayProtocol.Feature); } // Advertised so a client knows whether to hold a socket open or rely on its timer. Absence is // not an error — synchronising on a timer is the supported behaviour and the socket only makes // it early — which is why this is a feature flag rather than a version bump. See ADR 0012. if (events.Value.Enabled) { features.Add(VaultEvents.Feature); } return Task.FromResult(TypedResults.Ok(new MetaResponse( ServerVersion: ServerVersion, ApiVersions: [1], SyncProtocolVersion: SyncProtocolVersion, CryptoSpecVersion: CryptoSpec.CurrentAadVersion, Features: features, MinClientVersion: server.Value.MinClientVersion, MaxOperationsPerPush: sync.Value.MaxOperationsPerPush, MaxPayloadBytes: sync.Value.MaxPayloadBytes, MaxItemPayloadBytes: sync.Value.MaxItemPayloadBytes))); } private static string ServerVersion { get; } = typeof(GetMetaEndpoint).Assembly .GetCustomAttribute()?.InformationalVersion ?? "0.0.0"; } /// /// Everything a client needs to begin authenticating, from one URL. /// /// /// This is the onboarding story: the user types one server URL and the client discovers the /// identity provider, the client id and the relay from here. See ADR 0002. /// internal sealed class GetDodoSshConfigurationEndpoint( IOptions oidc, IOptions relay, IOptions server) : EndpointWithoutRequest> { /// public override void Configure() { Get("/.well-known/dodossh-configuration"); // Anonymous for the same reason as /api/v1/meta, and more so: this is the document that says // where the identity provider is. AllowAnonymous(); Description(b => b .WithName("GetDodoSshConfiguration") .WithSummary("Everything a client needs to begin authenticating, from one URL.")); } /// public override Task> ExecuteAsync(CancellationToken ct) { var relayOptions = relay.Value; return Task.FromResult(TypedResults.Ok(new DodoSshConfiguration( ApiBaseUrl: new Uri(server.Value.PublicBaseUrl, UriKind.Absolute), Oidc: new OidcConfiguration( Authority: new Uri(oidc.Value.Authority, UriKind.Absolute), ClientId: oidc.Value.ClientId, Scopes: [.. oidc.Value.Scopes], LoopbackRedirectPattern: oidc.Value.LoopbackRedirectPattern), Relay: new RelayConfiguration( Enabled: relayOptions.Enabled, WebSocketUrl: relayOptions.Enabled && relayOptions.WebSocketUrl is not null ? new Uri(relayOptions.WebSocketUrl, UriKind.Absolute) : null)))); } }