using System.Net; using System.Net.Http; namespace StellaOps.Concelier.Connector.Acsc.Configuration; /// /// Connector options governing ACSC feed access and retry behaviour. /// public sealed class AcscOptions { public const string HttpClientName = "acsc"; private static readonly TimeSpan DefaultRequestTimeout = TimeSpan.FromSeconds(45); private static readonly TimeSpan DefaultFailureBackoff = TimeSpan.FromMinutes(5); private static readonly TimeSpan DefaultInitialBackfill = TimeSpan.FromDays(120); public AcscOptions() { Feeds = new List { new() { Slug = "alerts", RelativePath = "/acsc/view-all-content/alerts/rss" }, new() { Slug = "advisories", RelativePath = "/acsc/view-all-content/advisories/rss" }, new() { Slug = "news", RelativePath = "/acsc/view-all-content/news/rss", Enabled = false }, new() { Slug = "publications", RelativePath = "/acsc/view-all-content/publications/rss", Enabled = false }, new() { Slug = "threats", RelativePath = "/acsc/view-all-content/threats/rss", Enabled = false }, }; } /// /// Base endpoint for direct ACSC fetches. /// public Uri BaseEndpoint { get; set; } = new("https://www.cyber.gov.au/", UriKind.Absolute); /// /// Optional relay endpoint used when Akamai terminates direct HTTP/2 connections. /// public Uri? RelayEndpoint { get; set; } /// /// Default mode when no preference has been captured in connector state. When true, the relay will be preferred for initial fetches. /// public bool PreferRelayByDefault { get; set; } /// /// If enabled, the connector may switch to the relay endpoint when direct fetches fail. /// public bool EnableRelayFallback { get; set; } = true; /// /// If set, the connector will always use the relay endpoint and skip direct attempts. /// public bool ForceRelay { get; set; } /// /// Timeout applied to fetch requests (overrides HttpClient default). /// public TimeSpan RequestTimeout { get; set; } = DefaultRequestTimeout; /// /// Backoff applied when marking fetch failures. /// public TimeSpan FailureBackoff { get; set; } = DefaultFailureBackoff; /// /// Look-back period used when deriving initial published cursors. /// public TimeSpan InitialBackfill { get; set; } = DefaultInitialBackfill; /// /// User-agent header sent with outbound requests. /// public string UserAgent { get; set; } = "StellaOps/Concelier (+https://stella-ops.org)"; /// /// RSS feeds requested during fetch. /// public IList Feeds { get; } /// /// HTTP version policy requested for outbound requests. /// public HttpVersionPolicy VersionPolicy { get; set; } = HttpVersionPolicy.RequestVersionOrLower; /// /// Default HTTP version requested when connecting to ACSC (defaults to HTTP/2 but allows downgrade). /// public Version RequestVersion { get; set; } = HttpVersion.Version20; public void Validate() { if (BaseEndpoint is null || !BaseEndpoint.IsAbsoluteUri) { throw new InvalidOperationException("ACSC BaseEndpoint must be an absolute URI."); } if (!BaseEndpoint.AbsoluteUri.EndsWith("/", StringComparison.Ordinal)) { throw new InvalidOperationException("ACSC BaseEndpoint must include a trailing slash."); } if (RelayEndpoint is not null && !RelayEndpoint.IsAbsoluteUri) { throw new InvalidOperationException("ACSC RelayEndpoint must be an absolute URI when specified."); } if (RelayEndpoint is not null && !RelayEndpoint.AbsoluteUri.EndsWith("/", StringComparison.Ordinal)) { throw new InvalidOperationException("ACSC RelayEndpoint must include a trailing slash when specified."); } if (RequestTimeout <= TimeSpan.Zero) { throw new InvalidOperationException("ACSC RequestTimeout must be positive."); } if (FailureBackoff < TimeSpan.Zero) { throw new InvalidOperationException("ACSC FailureBackoff cannot be negative."); } if (InitialBackfill <= TimeSpan.Zero) { throw new InvalidOperationException("ACSC InitialBackfill must be positive."); } if (string.IsNullOrWhiteSpace(UserAgent)) { throw new InvalidOperationException("ACSC UserAgent cannot be empty."); } if (Feeds.Count == 0) { throw new InvalidOperationException("At least one ACSC feed must be configured."); } var seen = new HashSet(StringComparer.OrdinalIgnoreCase); for (var i = 0; i < Feeds.Count; i++) { var feed = Feeds[i]; feed.Validate(i); if (!feed.Enabled) { continue; } if (!seen.Add(feed.Slug)) { throw new InvalidOperationException($"Duplicate ACSC feed slug '{feed.Slug}' detected. Slugs must be unique (case-insensitive)."); } } } }