quorate/packages/fsharp-runner/oci/RegistryAuth.cs

120 lines
4.2 KiB
C#
Raw Normal View History

using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace Quorate.Oci;
public sealed class RegistryAuth
{
internal RegistryAuth(string user, string password)
{
User = user;
Password = password;
}
internal string User { get; }
internal string Password { get; }
public static RegistryAuth Load(string path, string registry)
{
ArgumentException.ThrowIfNullOrWhiteSpace(path);
ArgumentException.ThrowIfNullOrWhiteSpace(registry);
try
{
using FileStream stream = File.OpenRead(path);
DockerConfigModel config = JsonSerializer.Deserialize(stream, OciJsonContext.Default.DockerConfigModel)
?? throw new InvalidDataException("Registry credential document is empty.");
Dictionary<string, DockerAuthModel> auths = config.Auths
?? throw new InvalidDataException("Registry credential document has no auths.");
if (!auths.TryGetValue(registry, out DockerAuthModel? entry) &&
!auths.TryGetValue($"https://{registry}", out entry))
{
throw new InvalidDataException($"Registry credentials are unavailable for {registry}.");
}
string encoded = entry.Auth
?? throw new InvalidDataException("Registry credentials are invalid.");
string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(encoded));
int separator = decoded.IndexOf(':', StringComparison.Ordinal);
if (separator < 1)
{
throw new InvalidDataException("Registry credentials are invalid.");
}
return new RegistryAuth(decoded[..separator], decoded[(separator + 1)..]);
}
catch (Exception error) when (error is JsonException or FormatException)
{
throw new InvalidDataException("Registry credentials are invalid.", error);
}
}
public override string ToString() => nameof(RegistryAuth);
internal AuthenticationHeaderValue Basic() => new(
"Basic",
Convert.ToBase64String(Encoding.UTF8.GetBytes($"{User}:{Password}")));
}
internal sealed record BearerChallenge(Uri Realm, string? Service, string? Scope)
{
public static BearerChallenge Parse(AuthenticationHeaderValue value)
{
if (!value.Scheme.Equals("Bearer", StringComparison.OrdinalIgnoreCase) ||
string.IsNullOrWhiteSpace(value.Parameter))
{
throw new InvalidDataException("Registry bearer challenge is invalid.");
}
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
int offset = 0;
string parameter = value.Parameter;
while (offset < parameter.Length)
{
while (offset < parameter.Length && (parameter[offset] == ',' || char.IsWhiteSpace(parameter[offset])))
{
offset++;
}
int equals = parameter.IndexOf('=', offset);
if (equals <= offset)
{
throw new InvalidDataException("Registry bearer challenge is invalid.");
}
string name = parameter[offset..equals].Trim();
offset = equals + 1;
if (offset >= parameter.Length || parameter[offset] != '"')
{
throw new InvalidDataException("Registry bearer challenge is invalid.");
}
int end = parameter.IndexOf('"', ++offset);
if (end < 0)
{
throw new InvalidDataException("Registry bearer challenge is invalid.");
}
values.Add(name, parameter[offset..end]);
offset = end + 1;
}
if (!values.TryGetValue("realm", out string? realmValue) ||
!Uri.TryCreate(realmValue, UriKind.Absolute, out Uri? realm) ||
realm.Scheme != Uri.UriSchemeHttps)
{
throw new InvalidDataException("Registry token endpoint must use HTTPS.");
}
values.TryGetValue("service", out string? service);
values.TryGetValue("scope", out string? scope);
return new BearerChallenge(realm, service, scope);
}
}