82 lines
2.8 KiB
C#
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);
|
|
}
|