using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; namespace JobTrackerApi.Services; public sealed class ExternalOrigin { private static readonly HashSet InternalHealthHosts = new(StringComparer.OrdinalIgnoreCase) { "backend", "localhost", "127.0.0.1", "::1", }; private readonly Uri _uri; private ExternalOrigin(Uri uri) { _uri = uri; BaseUrl = uri.GetComponents(UriComponents.SchemeAndServer, UriFormat.UriEscaped).TrimEnd('/'); } public string BaseUrl { get; } public bool UsesHttps => _uri.Scheme == Uri.UriSchemeHttps; public static ExternalOrigin FromConfiguration(IConfiguration configuration, bool production = false) => Parse(configuration["App:PublicBaseUrl"], production); public static ExternalOrigin Parse(string? value, bool production) { var raw = value?.Trim(); if (string.IsNullOrWhiteSpace(raw)) { if (production) throw new InvalidOperationException("App:PublicBaseUrl is required in Production."); raw = "http://localhost:3000"; } if (!Uri.TryCreate(raw, UriKind.Absolute, out var uri) || (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) || string.IsNullOrWhiteSpace(uri.Host) || !string.IsNullOrEmpty(uri.UserInfo) || uri.AbsolutePath != "/" || !string.IsNullOrEmpty(uri.Query) || !string.IsNullOrEmpty(uri.Fragment)) { throw new InvalidOperationException("App:PublicBaseUrl must be an absolute HTTP(S) origin without credentials, a path, query, or fragment."); } if (production && uri.Scheme != Uri.UriSchemeHttps) throw new InvalidOperationException("App:PublicBaseUrl must use HTTPS in Production."); return new ExternalOrigin(uri); } public string BuildPath(string pathAndQuery) { if (string.IsNullOrEmpty(pathAndQuery) || pathAndQuery[0] != '/') throw new ArgumentException("External paths must start with '/'.", nameof(pathAndQuery)); return $"{BaseUrl}{pathAndQuery}"; } public bool Matches(HostString host) { if (!string.Equals(host.Host, _uri.IdnHost, StringComparison.OrdinalIgnoreCase) && !string.Equals(host.Host, _uri.Host, StringComparison.OrdinalIgnoreCase)) return false; return _uri.IsDefaultPort ? host.Port is null || host.Port == _uri.Port : host.Port == _uri.Port; } public bool AllowsRequest(HostString host, PathString path) => Matches(host) || (path == "/health" && IsInternalHealthHost(host)); public static bool IsInternalHealthHost(HostString host) => InternalHealthHosts.Contains(host.Host); }