From 309c896376473ff8a50ca2c66b1ea1be3bfdc734 Mon Sep 17 00:00:00 2001 From: Daniel Robinson Date: Fri, 9 Aug 2024 13:14:48 +0100 Subject: [PATCH] The PR resolves an issue reported under https://github.com/oras-project/oras-dotnet/issues/133. While testing against https://hub.docker.com/_/registry, "Docker-Content-Digest" headers were found on response.Headers collection rather than response.Content.Headers. Aligned internal/private variables to use _camelCase as there was some inconsistencies. Signed-off-by: Daniel Robinson --- src/OrasProject.Oras/Content/Digest.cs | 6 +- .../Remote/HttpResponseMessageExtensions.cs | 6 +- .../Remote/RepositoryTest.cs | 99 ++++++++++--------- 3 files changed, 57 insertions(+), 54 deletions(-) diff --git a/src/OrasProject.Oras/Content/Digest.cs b/src/OrasProject.Oras/Content/Digest.cs index 919d130..bd0cb15 100644 --- a/src/OrasProject.Oras/Content/Digest.cs +++ b/src/OrasProject.Oras/Content/Digest.cs @@ -20,8 +20,8 @@ namespace OrasProject.Oras.Content; internal static class Digest { - private const string digestRegexPattern = @"[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+"; - private static readonly Regex digestRegex = new Regex(digestRegexPattern, RegexOptions.Compiled); + private const string _digestRegexPattern = @"[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+"; + private static readonly Regex _digestRegex = new Regex(_digestRegexPattern, RegexOptions.Compiled); /// /// Verifies the digest header and throws an exception if it is invalid. @@ -29,7 +29,7 @@ internal static class Digest /// internal static string Validate(string? digest) { - if (string.IsNullOrEmpty(digest) || !digestRegex.IsMatch(digest)) + if (string.IsNullOrEmpty(digest) || !_digestRegex.IsMatch(digest)) { throw new InvalidDigestException($"Invalid digest: {digest}"); } diff --git a/src/OrasProject.Oras/Registry/Remote/HttpResponseMessageExtensions.cs b/src/OrasProject.Oras/Registry/Remote/HttpResponseMessageExtensions.cs index f463831..eb08dca 100644 --- a/src/OrasProject.Oras/Registry/Remote/HttpResponseMessageExtensions.cs +++ b/src/OrasProject.Oras/Registry/Remote/HttpResponseMessageExtensions.cs @@ -25,6 +25,8 @@ namespace OrasProject.Oras.Registry.Remote; internal static class HttpResponseMessageExtensions { + private const string _dockerContentDigestHeader = "Docker-Content-Digest"; + /// /// Parses the error returned by the remote registry. /// @@ -75,7 +77,7 @@ public static async Task ParseErrorResponseAsync(this HttpResponseMes /// public static void VerifyContentDigest(this HttpResponseMessage response, string expected) { - if (!response.Content.Headers.TryGetValues("Docker-Content-Digest", out var digestValues)) + if (!response.Headers.TryGetValues(_dockerContentDigestHeader, out var digestValues)) { return; } @@ -154,7 +156,7 @@ public static async Task GenerateDescriptorAsync(this HttpResponseMe // 4. Validate Server Digest (if present) string? serverDigest = null; - if (response.Content.Headers.TryGetValues("Docker-Content-Digest", out var serverHeaderDigest)) + if (response.Headers.TryGetValues(_dockerContentDigestHeader, out var serverHeaderDigest)) { serverDigest = serverHeaderDigest.FirstOrDefault(); if (!string.IsNullOrEmpty(serverDigest)) diff --git a/tests/OrasProject.Oras.Tests/Remote/RepositoryTest.cs b/tests/OrasProject.Oras.Tests/Remote/RepositoryTest.cs index 528280c..ffe15b5 100644 --- a/tests/OrasProject.Oras.Tests/Remote/RepositoryTest.cs +++ b/tests/OrasProject.Oras.Tests/Remote/RepositoryTest.cs @@ -42,10 +42,11 @@ public struct TestIOStruct public bool errExpectedOnGET; } - private byte[] theAmazingBanClan = "Ban Gu, Ban Chao, Ban Zhao"u8.ToArray(); - private const string theAmazingBanDigest = "b526a4f2be963a2f9b0990c001255669eab8a254ab1a6e3f84f1820212ac7078"; + private byte[] _theAmazingBanClan = "Ban Gu, Ban Chao, Ban Zhao"u8.ToArray(); + private const string _theAmazingBanDigest = "b526a4f2be963a2f9b0990c001255669eab8a254ab1a6e3f84f1820212ac7078"; + + private const string _dockerContentDigestHeader = "Docker-Content-Digest"; - private const string dockerContentDigestHeader = "Docker-Content-Digest"; // The following truth table aims to cover the expected GET/HEAD request outcome // for all possible permutations of the client/server "containing a digest", for // both Manifests and Blobs. Where the results between the two differ, the index @@ -85,7 +86,7 @@ public struct TestIOStruct /// public static Dictionary GetTestIOStructMapForGetDescriptorClass() { - string correctDigest = $"sha256:{theAmazingBanDigest}"; + string correctDigest = $"sha256:{_theAmazingBanDigest}"; string incorrectDigest = $"sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; return new Dictionary @@ -194,7 +195,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(_dockerContentDigestHeader, blobDesc.Digest); return resp; } @@ -209,7 +210,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(_dockerContentDigestHeader, indexDesc.Digest); return resp; } @@ -289,7 +290,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(_dockerContentDigestHeader, blobDesc.Digest); resp.StatusCode = HttpStatusCode.Created; return resp; @@ -307,7 +308,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(_dockerContentDigestHeader, indexDesc.Digest); resp.StatusCode = HttpStatusCode.Created; return resp; } @@ -364,7 +365,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(_dockerContentDigestHeader, blobDesc.Digest); return res; } @@ -378,7 +379,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(_dockerContentDigestHeader, indexDesc.Digest); return res; } @@ -432,7 +433,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(_dockerContentDigestHeader, blobDesc.Digest); res.StatusCode = HttpStatusCode.Accepted; return res; } @@ -440,7 +441,7 @@ public async Task Repository_DeleteAsync() if (req.RequestUri!.AbsolutePath == "/v2/test/manifests/" + indexDesc.Digest) { indexDeleted = true; - // no "Docker-Content-Digest" header for manifest deletion + // no dockerContentDigestHeader header for manifest deletion res.StatusCode = HttpStatusCode.Accepted; return res; } @@ -509,7 +510,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(_dockerContentDigestHeader, indexDesc.Digest); return res; } @@ -583,7 +584,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(_dockerContentDigestHeader, indexDesc.Digest); return res; } @@ -601,7 +602,7 @@ public async Task Repository_TagAsync() { gotIndex = await req.Content.ReadAsByteArrayAsync(cancellationToken); } - res.Content.Headers.Add("Docker-Content-Digest", indexDesc.Digest); + res.Headers.Add(_dockerContentDigestHeader, indexDesc.Digest); res.StatusCode = HttpStatusCode.Created; return res; } @@ -656,7 +657,7 @@ public async Task Repository_PushReferenceAsync() { gotIndex = await req.Content.ReadAsByteArrayAsync(); } - res.Content.Headers.Add("Docker-Content-Digest", indexDesc.Digest); + res.Headers.Add(_dockerContentDigestHeader, indexDesc.Digest); res.StatusCode = HttpStatusCode.Created; return res; } @@ -723,7 +724,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(_dockerContentDigestHeader, indexDesc.Digest); return res; } @@ -887,7 +888,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(_dockerContentDigestHeader, blobDesc.Digest); return res; } @@ -950,7 +951,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(_dockerContentDigestHeader, blobDesc.Digest); return res; } @@ -977,12 +978,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(_dockerContentDigestHeader, 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(_dockerContentDigestHeader, blobDesc.Digest); res.StatusCode = HttpStatusCode.NotFound; return res; }; @@ -1044,7 +1045,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(_dockerContentDigestHeader, blobDesc.Digest); return res; } @@ -1108,7 +1109,7 @@ public async Task BlobStore_PushAsync() // read content into buffer var stream = req.Content!.ReadAsStream(cancellationToken); stream.Read(gotBlob); - res.Headers.Add("Docker-Content-Digest", blobDesc.Digest); + res.Headers.Add(_dockerContentDigestHeader, blobDesc.Digest); res.StatusCode = HttpStatusCode.Created; return res; } @@ -1161,7 +1162,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(_dockerContentDigestHeader, blobDesc.Digest); res.Content.Headers.Add("Content-Length", blobDesc.Size.ToString()); return res; } @@ -1210,7 +1211,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(_dockerContentDigestHeader, blobDesc.Digest); res.StatusCode = HttpStatusCode.Accepted; return res; } @@ -1265,7 +1266,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(_dockerContentDigestHeader, blobDesc.Digest); res.Content.Headers.Add("Content-Length", blobDesc.Size.ToString()); return res; } @@ -1327,7 +1328,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(_dockerContentDigestHeader, blobDesc.Digest); return res; } @@ -1411,7 +1412,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(_dockerContentDigestHeader, blobDesc.Digest); return res; } @@ -1426,12 +1427,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(_dockerContentDigestHeader, 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(_dockerContentDigestHeader, blobDesc.Digest); res.StatusCode = HttpStatusCode.NotFound; return res; }; @@ -1493,14 +1494,14 @@ public void GenerateBlobDescriptor_WithVariousDockerContentDigestHeaders() var resp = new HttpResponseMessage(); if (method == HttpMethod.Get) { - resp.Content = new ByteArrayContent(theAmazingBanClan); + 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 @@ -1590,7 +1591,7 @@ public async Task ManifestStore_FetchAsync() } res.Content = new ByteArrayContent(manifest); res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageManifest }); - res.Content.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); + res.Headers.Add(_dockerContentDigestHeader, new string[] { manifestDesc.Digest }); return res; } return new HttpResponseMessage(HttpStatusCode.NotFound); @@ -1688,7 +1689,7 @@ public async Task ManifestStore_PushAsync() (await req.Content.ReadAsByteArrayAsync()).CopyTo(buf, 0); gotManifest = buf; } - res.Content.Headers.Add("Docker-Content-Digest", new string[] { manifestDesc.Digest }); + res.Headers.Add(_dockerContentDigestHeader, new string[] { manifestDesc.Digest }); res.StatusCode = HttpStatusCode.Created; return res; } @@ -1737,7 +1738,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(_dockerContentDigestHeader, new string[] { manifestDesc.Digest }); res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageManifest }); res.Content.Headers.Add("Content-Length", new string[] { manifest.Length.ToString() }); return res; @@ -1802,7 +1803,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(_dockerContentDigestHeader, new string[] { manifestDesc.Digest }); res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageManifest }); return res; } @@ -1858,7 +1859,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(_dockerContentDigestHeader, new string[] { manifestDesc.Digest }); res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageManifest }); res.Content.Headers.Add("Content-Length", new string[] { manifest.Length.ToString() }); return res; @@ -1928,7 +1929,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(_dockerContentDigestHeader, new string[] { manifestDesc.Digest }); res.Content.Headers.Add("Content-Type", new string[] { MediaType.ImageManifest }); return res; } @@ -2019,7 +2020,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(_dockerContentDigestHeader, new string[] { indexDesc.Digest }); res.Content.Headers.Add("Content-Type", new string[] { indexDesc.MediaType }); return res; } @@ -2037,7 +2038,7 @@ public async Task ManifestStore_TagAsync() gotIndex = buf; } - res.Content.Headers.Add("Docker-Content-Digest", new string[] { indexDesc.Digest }); + res.Headers.Add(_dockerContentDigestHeader, new string[] { indexDesc.Digest }); res.StatusCode = HttpStatusCode.Created; return res; } @@ -2102,7 +2103,7 @@ public async Task ManifestStore_PushReferenceAsync() gotIndex = buf; } - res.Content.Headers.Add("Docker-Content-Digest", new string[] { indexDesc.Digest }); + res.Headers.Add(_dockerContentDigestHeader, new string[] { indexDesc.Digest }); res.StatusCode = HttpStatusCode.Created; return res; } @@ -2168,12 +2169,12 @@ public async Task CopyFromRepositoryToMemory() { res.Content = new ByteArrayContent(exampleManifest); res.Content.Headers.Add("Content-Type", MediaType.Descriptor); - res.Content.Headers.Add("Docker-Content-Digest", exampleManifestDescriptor.Digest); + res.Headers.Add(_dockerContentDigestHeader, exampleManifestDescriptor.Digest); res.Content.Headers.Add("Content-Length", exampleManifest.Length.ToString()); return res; } res.Content.Headers.Add("Content-Type", MediaType.Descriptor); - res.Content.Headers.Add("Docker-Content-Digest", exampleManifestDescriptor.Digest); + res.Headers.Add(_dockerContentDigestHeader, exampleManifestDescriptor.Digest); res.Content.Headers.Add("Content-Length", exampleManifest.Length.ToString()); return res; } @@ -2193,7 +2194,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(_dockerContentDigestHeader, digest); return res; } @@ -2235,14 +2236,14 @@ public async Task ManifestStore_generateDescriptorWithVariousDockerContentDigest var resp = new HttpResponseMessage(); if (method == HttpMethod.Get) { - resp.Content = new ByteArrayContent(theAmazingBanClan); + 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() {