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).");
}
}
}
}