From ae83e73e07b67c21103178307d03ed728b34390c Mon Sep 17 00:00:00 2001 From: Sajay Antony Date: Tue, 12 Dec 2023 18:23:53 -0800 Subject: [PATCH 1/2] sample: add manifest fetch --- Oras.sln | 70 +++++++++++++--------- samples/ManifestFetch/ManifestFetch.csproj | 14 +++++ samples/ManifestFetch/Program.cs | 14 +++++ 3 files changed, 70 insertions(+), 28 deletions(-) create mode 100644 samples/ManifestFetch/ManifestFetch.csproj create mode 100644 samples/ManifestFetch/Program.cs diff --git a/Oras.sln b/Oras.sln index 8c70aed..ac0b5a9 100644 --- a/Oras.sln +++ b/Oras.sln @@ -1,28 +1,42 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oras.Tests", "Oras.Tests\Oras.Tests.csproj", "{70DFCE13-53AA-4CC2-8E1A-F73A1344A0CB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oras", "Oras\Oras.csproj", "{547F152F-6F4D-4E5A-AD3A-C3BC5CDCFD27}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {70DFCE13-53AA-4CC2-8E1A-F73A1344A0CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {70DFCE13-53AA-4CC2-8E1A-F73A1344A0CB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {70DFCE13-53AA-4CC2-8E1A-F73A1344A0CB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {70DFCE13-53AA-4CC2-8E1A-F73A1344A0CB}.Release|Any CPU.Build.0 = Release|Any CPU - {547F152F-6F4D-4E5A-AD3A-C3BC5CDCFD27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {547F152F-6F4D-4E5A-AD3A-C3BC5CDCFD27}.Debug|Any CPU.Build.0 = Debug|Any CPU - {547F152F-6F4D-4E5A-AD3A-C3BC5CDCFD27}.Release|Any CPU.ActiveCfg = Release|Any CPU - {547F152F-6F4D-4E5A-AD3A-C3BC5CDCFD27}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oras.Tests", "Oras.Tests\Oras.Tests.csproj", "{70DFCE13-53AA-4CC2-8E1A-F73A1344A0CB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oras", "Oras\Oras.csproj", "{547F152F-6F4D-4E5A-AD3A-C3BC5CDCFD27}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{D198F8D2-3EBC-4ADE-A794-FF64800AAC25}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManifestFetch", "samples\ManifestFetch\ManifestFetch.csproj", "{B82210C7-222B-4632-AE38-7BAEA6185654}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {70DFCE13-53AA-4CC2-8E1A-F73A1344A0CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70DFCE13-53AA-4CC2-8E1A-F73A1344A0CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70DFCE13-53AA-4CC2-8E1A-F73A1344A0CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70DFCE13-53AA-4CC2-8E1A-F73A1344A0CB}.Release|Any CPU.Build.0 = Release|Any CPU + {547F152F-6F4D-4E5A-AD3A-C3BC5CDCFD27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {547F152F-6F4D-4E5A-AD3A-C3BC5CDCFD27}.Debug|Any CPU.Build.0 = Debug|Any CPU + {547F152F-6F4D-4E5A-AD3A-C3BC5CDCFD27}.Release|Any CPU.ActiveCfg = Release|Any CPU + {547F152F-6F4D-4E5A-AD3A-C3BC5CDCFD27}.Release|Any CPU.Build.0 = Release|Any CPU + {B82210C7-222B-4632-AE38-7BAEA6185654}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B82210C7-222B-4632-AE38-7BAEA6185654}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B82210C7-222B-4632-AE38-7BAEA6185654}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B82210C7-222B-4632-AE38-7BAEA6185654}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B82210C7-222B-4632-AE38-7BAEA6185654} = {D198F8D2-3EBC-4ADE-A794-FF64800AAC25} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1407904E-69B8-43DD-8CE5-67ACBCAB2416} + EndGlobalSection +EndGlobal diff --git a/samples/ManifestFetch/ManifestFetch.csproj b/samples/ManifestFetch/ManifestFetch.csproj new file mode 100644 index 0000000..a7e38ff --- /dev/null +++ b/samples/ManifestFetch/ManifestFetch.csproj @@ -0,0 +1,14 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + diff --git a/samples/ManifestFetch/Program.cs b/samples/ManifestFetch/Program.cs new file mode 100644 index 0000000..893a981 --- /dev/null +++ b/samples/ManifestFetch/Program.cs @@ -0,0 +1,14 @@ +var reference = "ghcr.io/oras-project/oras:v1.1.0"; +var repo = new Oras.Remote.Repository(reference); + +// Get the content digest +var desc = await repo.ResolveAsync(reference); +Console.WriteLine("Digest: {0}", desc.Digest); + +// Retrive the manifest content +var content = await repo.Manifests().FetchReferenceAsync(reference); +using (var reader = new StreamReader(content.Stream)) +{ + var output = await reader.ReadToEndAsync(); + Console.WriteLine(output); +} \ No newline at end of file From 7ded2b0478e73983f4e71c88ace6fef0c9c0d1e2 Mon Sep 17 00:00:00 2001 From: Sajay Antony Date: Tue, 12 Dec 2023 18:24:45 -0800 Subject: [PATCH 2/2] auth: enable bearer token --- Oras.Tests/RemoteTest/RepositoryTest.cs | 82 ++++++++-------- Oras/Content/DigestUtility.cs | 9 +- Oras/Remote/RegistryMessageHandler.cs | 122 ++++++++++++++++++++++++ Oras/Remote/RemoteReference.cs | 10 ++ Oras/Remote/Repository.cs | 19 ++-- 5 files changed, 187 insertions(+), 55 deletions(-) create mode 100644 Oras/Remote/RegistryMessageHandler.cs diff --git a/Oras.Tests/RemoteTest/RepositoryTest.cs b/Oras.Tests/RemoteTest/RepositoryTest.cs index 5b1b60c..cd738cb 100644 --- a/Oras.Tests/RemoteTest/RepositoryTest.cs +++ b/Oras.Tests/RemoteTest/RepositoryTest.cs @@ -171,7 +171,7 @@ public async Task Repository_FetchAsync() { resp.Content = new ByteArrayContent(blob); resp.Content.Headers.Add("Content-Type", "application/octet-stream"); - resp.Content.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + resp.Headers.Add("Docker-Content-Digest", blobDesc.Digest); return resp; } @@ -186,7 +186,7 @@ public async Task Repository_FetchAsync() resp.Content = new ByteArrayContent(index); resp.Content.Headers.Add("Content-Type", indexDesc.MediaType); - resp.Content.Headers.Add("Docker-Content-Digest", indexDesc.Digest); + resp.Headers.Add("Docker-Content-Digest", indexDesc.Digest); return resp; } @@ -263,7 +263,7 @@ public async Task Repository_PushAsync() var stream = req.Content!.ReadAsStream(cancellationToken); stream.Read(gotBlob); - resp.Content.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + resp.Headers.Add("Docker-Content-Digest", blobDesc.Digest); resp.StatusCode = HttpStatusCode.Created; return resp; @@ -281,7 +281,7 @@ public async Task Repository_PushAsync() var stream = req.Content!.ReadAsStream(cancellationToken); stream.Read(gotIndex); - resp.Content.Headers.Add("Docker-Content-Digest", indexDesc.Digest); + resp.Headers.Add("Docker-Content-Digest", indexDesc.Digest); resp.StatusCode = HttpStatusCode.Created; return resp; } @@ -335,7 +335,7 @@ public async Task Repository_ExistsAsync() { res.Content.Headers.Add("Content-Type", "application/octet-stream"); res.Content.Headers.Add("Content-Length", blobDesc.Size.ToString()); - res.Content.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + res.Headers.Add("Docker-Content-Digest", blobDesc.Digest); return res; } @@ -349,7 +349,7 @@ public async Task Repository_ExistsAsync() res.Content.Headers.Add("Content-Type", indexDesc.MediaType); res.Content.Headers.Add("Content-Length", indexDesc.Size.ToString()); - res.Content.Headers.Add("Docker-Content-Digest", indexDesc.Digest); + res.Headers.Add("Docker-Content-Digest", indexDesc.Digest); return res; } @@ -400,7 +400,7 @@ public async Task Repository_DeleteAsync() if (req.RequestUri!.AbsolutePath == "/v2/test/blobs/" + blobDesc.Digest) { blobDeleted = true; - res.Content.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + res.Headers.Add("Docker-Content-Digest", blobDesc.Digest); res.StatusCode = HttpStatusCode.Accepted; return res; } @@ -474,7 +474,7 @@ public async Task Repository_ResolveAsync() res.Content.Headers.Add("Content-Type", indexDesc.MediaType); res.Content.Headers.Add("Content-Length", indexDesc.Size.ToString()); - res.Content.Headers.Add("Docker-Content-Digest", indexDesc.Digest); + res.Headers.Add("Docker-Content-Digest", indexDesc.Digest); return res; } @@ -545,7 +545,7 @@ public async Task Repository_TagAsync() res.Content = new ByteArrayContent(index); res.Content.Headers.Add("Content-Type", indexDesc.MediaType); - res.Content.Headers.Add("Docker-Content-Digest", indexDesc.Digest); + res.Headers.Add("Docker-Content-Digest", indexDesc.Digest); return res; } @@ -560,7 +560,7 @@ public async Task Repository_TagAsync() } gotIndex = req.Content?.ReadAsByteArrayAsync().Result; - res.Content.Headers.Add("Docker-Content-Digest", indexDesc.Digest); + res.Headers.Add("Docker-Content-Digest", indexDesc.Digest); res.StatusCode = HttpStatusCode.Created; return res; } @@ -609,7 +609,7 @@ public async Task Repository_PushReferenceAsync() } gotIndex = req.Content?.ReadAsByteArrayAsync().Result; - res.Content.Headers.Add("Docker-Content-Digest", indexDesc.Digest); + res.Headers.Add("Docker-Content-Digest", indexDesc.Digest); res.StatusCode = HttpStatusCode.Created; return res; } @@ -673,7 +673,7 @@ public async Task Repository_FetchReferenceAsyc() res.Content = new ByteArrayContent(index); res.Content.Headers.Add("Content-Type", indexDesc.MediaType); - res.Content.Headers.Add("Docker-Content-Digest", indexDesc.Digest); + res.Headers.Add("Docker-Content-Digest", indexDesc.Digest); return res; } @@ -832,7 +832,7 @@ public async Task BlobStore_FetchAsync() { res.Content = new ByteArrayContent(blob); res.Content.Headers.Add("Content-Type", "application/octet-stream"); - res.Content.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + res.Headers.Add("Docker-Content-Digest", blobDesc.Digest); return res; } @@ -892,7 +892,7 @@ public async Task BlobStore_FetchAsync_CanSeek() res.StatusCode = HttpStatusCode.OK; res.Content = new ByteArrayContent(blob); res.Content.Headers.Add("Content-Type", "application/octet-stream"); - res.Content.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + res.Headers.Add("Docker-Content-Digest", blobDesc.Digest); return res; } @@ -919,12 +919,12 @@ public async Task BlobStore_FetchAsync_CanSeek() res.StatusCode = HttpStatusCode.PartialContent; res.Content = new ByteArrayContent(blob[(int)start..(int)end]); res.Content.Headers.Add("Content-Type", "application/octet-stream"); - res.Content.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + res.Headers.Add("Docker-Content-Digest", blobDesc.Digest); return res; } res.Content.Headers.Add("Content-Type", "application/octet-stream"); - res.Content.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + res.Headers.Add("Docker-Content-Digest", blobDesc.Digest); res.StatusCode = HttpStatusCode.NotFound; return res; }; @@ -983,7 +983,7 @@ public async Task BlobStore_FetchAsync_ZeroSizedBlob() } res.Content.Headers.Add("Content-Type", "application/octet-stream"); - res.Content.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + res.Headers.Add("Docker-Content-Digest", blobDesc.Digest); return res; } @@ -1094,7 +1094,7 @@ public async Task BlobStore_ExistsAsync() if (req.RequestUri?.AbsolutePath == $"/v2/test/blobs/{blobDesc.Digest}") { res.Content.Headers.Add("Content-Type", "application/octet-stream"); - res.Content.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + res.Headers.Add("Docker-Content-Digest", blobDesc.Digest); res.Content.Headers.Add("Content-Length", blobDesc.Size.ToString()); return res; } @@ -1140,7 +1140,7 @@ public async Task BlobStore_DeleteAsync() if (req.RequestUri?.AbsolutePath == $"/v2/test/blobs/{blobDesc.Digest}") { blobDeleted = true; - res.Content.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + res.Headers.Add("Docker-Content-Digest", blobDesc.Digest); res.StatusCode = HttpStatusCode.Accepted; return res; } @@ -1192,7 +1192,7 @@ public async Task BlobStore_ResolveAsync() if (req.RequestUri?.AbsolutePath == $"/v2/test/blobs/{blobDesc.Digest}") { res.Content.Headers.Add("Content-Type", "application/octet-stream"); - res.Content.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + res.Headers.Add("Docker-Content-Digest", blobDesc.Digest); res.Content.Headers.Add("Content-Length", blobDesc.Size.ToString()); return res; } @@ -1251,7 +1251,7 @@ public async Task BlobStore_FetchReferenceAsync() { res.Content = new ByteArrayContent(blob); res.Content.Headers.Add("Content-Type", "application/octet-stream"); - res.Content.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + res.Headers.Add("Docker-Content-Digest", blobDesc.Digest); return res; } @@ -1332,7 +1332,7 @@ public async Task BlobStore_FetchReferenceAsync_Seek() res.StatusCode = HttpStatusCode.OK; res.Content = new ByteArrayContent(blob); res.Content.Headers.Add("Content-Type", "application/octet-stream"); - res.Content.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + res.Headers.Add("Docker-Content-Digest", blobDesc.Digest); return res; } @@ -1347,12 +1347,12 @@ public async Task BlobStore_FetchReferenceAsync_Seek() res.StatusCode = HttpStatusCode.PartialContent; res.Content = new ByteArrayContent(blob[(int)start..]); res.Content.Headers.Add("Content-Type", "application/octet-stream"); - res.Content.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + res.Headers.Add("Docker-Content-Digest", blobDesc.Digest); return res; } res.Content.Headers.Add("Content-Type", "application/octet-stream"); - res.Content.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + res.Headers.Add("Docker-Content-Digest", blobDesc.Digest); res.StatusCode = HttpStatusCode.NotFound; return res; }; @@ -1418,12 +1418,12 @@ public void GenerateBlobDescriptor_WithVariousDockerContentDigestHeaders() { resp.Content = new ByteArrayContent(theAmazingBanClan); resp.Content.Headers.Add("Content-Type", new string[] { "application/vnd.docker.distribution.manifest.v2+json" }); - resp.Content.Headers.Add(dockerContentDigestHeader, new string[] { dcdIOStruct.serverCalculatedDigest }); + resp.Headers.Add(dockerContentDigestHeader, new string[] { dcdIOStruct.serverCalculatedDigest }); } - if (!resp.Content.Headers.TryGetValues(dockerContentDigestHeader, out IEnumerable? values)) + if (!resp.Headers.TryGetValues(dockerContentDigestHeader, out IEnumerable? values)) { resp.Content.Headers.Add("Content-Type", new string[] { "application/vnd.docker.distribution.manifest.v2+json" }); - resp.Content.Headers.Add(dockerContentDigestHeader, new string[] { dcdIOStruct.serverCalculatedDigest }); + resp.Headers.Add(dockerContentDigestHeader, new string[] { dcdIOStruct.serverCalculatedDigest }); resp.RequestMessage = new HttpRequestMessage() { Method = method @@ -1514,7 +1514,7 @@ public async Task ManifestStore_FetchAsync() } res.Content = new ByteArrayContent(manifest); res.Content.Headers.Add("Content-Type", new string[] { OCIMediaTypes.ImageManifest }); - res.Content.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); + res.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); return res; } return new HttpResponseMessage(HttpStatusCode.NotFound); @@ -1572,7 +1572,7 @@ public async Task ManifestStore_PushAsync() req.Content.ReadAsByteArrayAsync().Result.CopyTo(buf, 0); gotManifest = buf; } - res.Content.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); + res.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); res.StatusCode = HttpStatusCode.Created; return res; } @@ -1618,7 +1618,7 @@ public async Task ManifestStore_ExistAsync() { return new HttpResponseMessage(HttpStatusCode.BadRequest); } - res.Content.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); + res.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); res.Content.Headers.Add("Content-Type", new string[] { OCIMediaTypes.ImageManifest }); res.Content.Headers.Add("Content-Length", new string[] { manifest.Length.ToString() }); return res; @@ -1680,7 +1680,7 @@ public async Task ManifestStore_DeleteAsync() return new HttpResponseMessage(HttpStatusCode.BadRequest); } res.Content = new ByteArrayContent(manifest); - res.Content.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); + res.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); res.Content.Headers.Add("Content-Type", new string[] { OCIMediaTypes.ImageManifest }); return res; } @@ -1733,7 +1733,7 @@ public async Task ManifestStore_ResolveAsync() { return new HttpResponseMessage(HttpStatusCode.BadRequest); } - res.Content.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); + res.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); res.Content.Headers.Add("Content-Type", new string[] { OCIMediaTypes.ImageManifest }); res.Content.Headers.Add("Content-Length", new string[] { manifest.Length.ToString() }); return res; @@ -1800,7 +1800,7 @@ public async Task ManifestStore_FetchReferenceAsync() return new HttpResponseMessage(HttpStatusCode.BadRequest); } res.Content = new ByteArrayContent(manifest); - res.Content.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); + res.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); res.Content.Headers.Add("Content-Type", new string[] { OCIMediaTypes.ImageManifest }); return res; } @@ -1888,7 +1888,7 @@ public async Task ManifestStore_TagAsync() return new HttpResponseMessage(HttpStatusCode.BadRequest); } res.Content = new ByteArrayContent(index); - res.Content.Headers.Add("Docker-Content-Digest", new string[] { indexDesc.Digest }); + res.Headers.Add("Docker-Content-Digest", new string[] { indexDesc.Digest }); res.Content.Headers.Add("Content-Type", new string[] { indexDesc.MediaType }); return res; } @@ -1906,7 +1906,7 @@ public async Task ManifestStore_TagAsync() gotIndex = buf; } - res.Content.Headers.Add("Docker-Content-Digest", new string[] { indexDesc.Digest }); + res.Headers.Add("Docker-Content-Digest", new string[] { indexDesc.Digest }); res.StatusCode = HttpStatusCode.Created; return res; } @@ -1968,7 +1968,7 @@ public async Task ManifestStore_PushReferenceAsync() gotIndex = buf; } - res.Content.Headers.Add("Docker-Content-Digest", new string[] { indexDesc.Digest }); + res.Headers.Add("Docker-Content-Digest", new string[] { indexDesc.Digest }); res.StatusCode = HttpStatusCode.Created; return res; } @@ -2031,12 +2031,12 @@ public async Task CopyFromRepositoryToMemory() { res.Content = new ByteArrayContent(exampleManifest); res.Content.Headers.Add("Content-Type", OCIMediaTypes.Descriptor); - res.Content.Headers.Add("Docker-Content-Digest", exampleManifestDescriptor.Digest); + res.Headers.Add("Docker-Content-Digest", exampleManifestDescriptor.Digest); res.Content.Headers.Add("Content-Length", exampleManifest.Length.ToString()); return res; } res.Content.Headers.Add("Content-Type", OCIMediaTypes.Descriptor); - res.Content.Headers.Add("Docker-Content-Digest", exampleManifestDescriptor.Digest); + res.Headers.Add("Docker-Content-Digest", exampleManifestDescriptor.Digest); res.Content.Headers.Add("Content-Length", exampleManifest.Length.ToString()); return res; } @@ -2056,7 +2056,7 @@ public async Task CopyFromRepositoryToMemory() res.Content.Headers.Add("Content-Length", content.Length.ToString()); } - res.Content.Headers.Add("Docker-Content-Digest", digest); + res.Headers.Add("Docker-Content-Digest", digest); return res; } @@ -2102,12 +2102,12 @@ public async Task ManifestStore_generateDescriptorWithVariousDockerContentDigest { resp.Content = new ByteArrayContent(theAmazingBanClan); resp.Content.Headers.Add("Content-Type", new string[] { "application/vnd.docker.distribution.manifest.v2+json" }); - resp.Content.Headers.Add(dockerContentDigestHeader, new string[] { dcdIOStruct.serverCalculatedDigest }); + resp.Headers.Add(dockerContentDigestHeader, new string[] { dcdIOStruct.serverCalculatedDigest }); } else { resp.Content.Headers.Add("Content-Type", new string[] { "application/vnd.docker.distribution.manifest.v2+json" }); - resp.Content.Headers.Add(dockerContentDigestHeader, new string[] { dcdIOStruct.serverCalculatedDigest }); + resp.Headers.Add(dockerContentDigestHeader, new string[] { dcdIOStruct.serverCalculatedDigest }); } resp.RequestMessage = new HttpRequestMessage() { diff --git a/Oras/Content/DigestUtility.cs b/Oras/Content/DigestUtility.cs index d5b4ea9..74f8c50 100644 --- a/Oras/Content/DigestUtility.cs +++ b/Oras/Content/DigestUtility.cs @@ -1,5 +1,6 @@ using Oras.Exceptions; using System; +using System.Net.NetworkInformation; using System.Security.Cryptography; using System.Text.RegularExpressions; @@ -12,6 +13,7 @@ internal static class DigestUtility /// digestRegexp checks the digest. /// private const string digestRegexp = @"[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+"; + private static readonly Regex digestRegex = new Regex(digestRegexp, RegexOptions.Compiled); /// /// ParseDigest verifies the digest header and throws an exception if it is invalid. @@ -19,7 +21,7 @@ internal static class DigestUtility /// internal static string ParseDigest(string digest) { - if (!Regex.IsMatch(digest, digestRegexp)) + if (IsDigest(digest) == false) { throw new InvalidDigestException($"Invalid digest: {digest}"); } @@ -27,6 +29,11 @@ internal static string ParseDigest(string digest) return digest; } + internal static bool IsDigest(string digest) + { + return !String.IsNullOrEmpty(digest) && digestRegex.IsMatch(digest); + } + /// /// CalculateSHA256DigestFromBytes generates a SHA256 digest from a byte array. /// diff --git a/Oras/Remote/RegistryMessageHandler.cs b/Oras/Remote/RegistryMessageHandler.cs new file mode 100644 index 0000000..8efe51f --- /dev/null +++ b/Oras/Remote/RegistryMessageHandler.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Net; +using System.Linq; + +namespace Oras.Remote +{ + internal class RegistryMessageHandler : HttpClientHandler + { + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var res = await base.SendAsync(request, cancellationToken); + + // If this is unauthorized there should be a challenge header + if (res.StatusCode == HttpStatusCode.Unauthorized) + { + var token = await GetAccessTokenAsync(res, cancellationToken); + request.Headers.Add("Authorization", "Bearer " + token); + return await base.SendAsync(request, cancellationToken); + } + + return res; + } + + public const string AuthenticateHeaderKey = "Www-Authenticate"; + + /// + /// Get a docker access token for a URI using OAuth2 flow with retry. + /// + public async Task GetAccessTokenAsync( + HttpResponseMessage challenge, + CancellationToken cancellationToken) + { + var uri = challenge.RequestMessage?.RequestUri; + + /* + * Www-Authenticate: Bearer realm="https://auth.docker.io/token", + * service="registry.docker.io",scope="repository:library/official-app:pull" + */ + if (challenge.StatusCode != HttpStatusCode.Unauthorized + || !challenge.Headers.Contains(AuthenticateHeaderKey)) + { + throw new Exception($"URI {uri} did not issue a challenge with status code: {challenge.StatusCode}"); + } + + var authenticateHeaderValue = challenge.Headers.GetValues(AuthenticateHeaderKey).FirstOrDefault(); + + if (string.IsNullOrEmpty(authenticateHeaderValue)) + { + throw new Exception($"Empty authenticate header."); + } + + var authenticate = authenticateHeaderValue.Split(' '); + if (authenticate.Length != 2 || string.Compare(authenticate[0], "Bearer", true) < 0) + { + throw new Exception($"URI {uri} did not return correct authenticate header {authenticateHeaderValue}."); + } + + var tokens = authenticate[1].Split(',').Select(t => + { + return t.Trim().Split('='); + }).ToDictionary(t => t[0], t => t[1]); + + if (!(tokens.ContainsKey("realm") + && tokens.ContainsKey("service") + && tokens.ContainsKey("scope"))) + { + throw new Exception($"URI {uri} did not return authenticate header with necessary fields {authenticateHeaderValue}."); + } + + var authUri = $"{tokens["realm"].Trim('"')}?service={tokens["service"].Trim('"')}&scope={tokens["scope"].Trim('"')}"; + + // handle retries + // create request message for authUri + var requestMsg = new HttpRequestMessage(HttpMethod.Get, authUri); + var response = await base.SendAsync(requestMsg, cancellationToken); + + if (response.IsSuccessStatusCode) + { + var strToken = await response?.Content?.ReadAsStringAsync(); + + var oAuthToken = JsonSerializer.Deserialize(strToken); + if (string.IsNullOrEmpty(oAuthToken?.Token)) + { + throw new Exception($"URI {authUri} could not return a valid access token."); + } + + return oAuthToken.Token; + } + else if (response.StatusCode == HttpStatusCode.NotFound) + { + throw new Exception($"Request failed with status code: {response.StatusCode}."); + } + else + { + throw new Exception($"Request failed with status code: {response.StatusCode}."); + } + } + } + + class OAuthToken + { + [JsonPropertyName("token")] + public string Token { get; set; } + + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } + + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } + + [JsonPropertyName("issued_at")] + public string IssuedAt { get; set; } + } +} diff --git a/Oras/Remote/RemoteReference.cs b/Oras/Remote/RemoteReference.cs index be2b32a..97bed19 100644 --- a/Oras/Remote/RemoteReference.cs +++ b/Oras/Remote/RemoteReference.cs @@ -196,5 +196,15 @@ public string Digest() return Reference; } + /// + /// IsDigest returns if the reference part is of a Digest form + /// + /// + public bool IsDigest() + { + return DigestUtility.IsDigest(Reference); + } + + } } diff --git a/Oras/Remote/Repository.cs b/Oras/Remote/Repository.cs index 6220f1a..2eec4a3 100644 --- a/Oras/Remote/Repository.cs +++ b/Oras/Remote/Repository.cs @@ -9,6 +9,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Runtime.InteropServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -60,8 +61,8 @@ public class Repository : IRepository, IRepositoryOption /// public Repository(string reference) { - RemoteReference = RemoteReference.ParseReference(reference); - HttpClient = new HttpClient(); + RemoteReference = RemoteReference.ParseReference(reference); + HttpClient = new HttpClient(new RegistryMessageHandler()); HttpClient.DefaultRequestHeaders.Add("User-Agent", new string[] { "oras-dotnet" }); } @@ -340,7 +341,7 @@ internal async Task DeleteAsync(Descriptor target, bool isManifest, Cancellation /// internal static void VerifyContentDigest(HttpResponseMessage resp, string expected) { - if (!resp.Content.Headers.TryGetValues("Docker-Content-Digest", out var digestValues)) return; + if (!resp.Headers.TryGetValues("Docker-Content-Digest", out var digestValues)) return; var digestStr = digestValues.FirstOrDefault(); if (string.IsNullOrEmpty(digestStr)) { @@ -628,18 +629,10 @@ public async Task GenerateDescriptor(HttpResponseMessage res, Remote } // 3. Validate Client Reference - string refDigest = string.Empty; - try - { - refDigest = reference.Digest(); - } - catch (Exception) - { - } - + string refDigest = reference.IsDigest() ? reference.Digest() : string.Empty; // 4. Validate Server Digest (if present) - res.Content.Headers.TryGetValues("Docker-Content-Digest", out IEnumerable serverHeaderDigest); + res.Headers.TryGetValues("Docker-Content-Digest", out IEnumerable serverHeaderDigest); var serverDigest = serverHeaderDigest?.First(); if (!string.IsNullOrEmpty(serverDigest)) {