Files
jobtrackingapp/JobTrackerApi/Services/ExternalOrigin.cs
T

82 lines
2.8 KiB
C#

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
namespace JobTrackerApi.Services;
public sealed class ExternalOrigin
{
private static readonly HashSet<string> 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);
}