120 lines
4.2 KiB
C#
120 lines
4.2 KiB
C#
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);
|
|
}
|
|
}
|