Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,29 @@ on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
dotnet-version: [8.x]

steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Dotnet
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v4
with:
dotnet-version: 6.0.100
dotnet-version: ${{ matrix.dotnet-version }}

- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v0.9.7
uses: gittools/actions/gitversion/setup@v1.1.1
with:
versionSpec: '5.x'
versionSpec: '5.x'

- name: GitVersion
id: gitversion
uses: gittools/actions/gitversion/execute@v0.9.7
uses: gittools/actions/gitversion/execute@v1.1.1
with:
useConfigFile: true

Expand All @@ -34,4 +37,5 @@ jobs:
- name: Publish
if: github.event_name != 'pull_request' && (github.ref_name == 'master')
run: |
dotnet nuget push **/*.nupkg --source 'https://api.nuget.org/v3/index.json' -k ${{ secrets.NUGETKEY }}
dotnet nuget push **/*.nupkg --source 'https://api.nuget.org/v3/index.json' -k ${{ secrets.NUGETKEY }} --skip-duplicate
dotnet nuget push **/*.snupkg --source 'https://api.nuget.org/v3/index.json' -k ${{ secrets.NUGETKEY }} --skip-duplicate
3 changes: 1 addition & 2 deletions Docker.Registry.DotNet.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32414.318
Expand All @@ -19,7 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 - Assets", "0 - Assets",
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3 - Samples", "3 - Samples", "{BBCE947D-B371-4738-98A2-F3FA9E934BF6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DockerRegistryExplorer", "samples\DockerRegistryExplorer\DockerRegistryExplorer.csproj", "{513C7B92-BFAF-4E0A-B8D9-FC0E7283CD62}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DockerRegistryExplorer", "samples\DockerRegistryExplorer\DockerRegistryExplorer.csproj", "{513C7B92-BFAF-4E0A-B8D9-FC0E7283CD62}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.Registry.Cli", "samples\Docker.Registry.Cli\Docker.Registry.Cli.csproj", "{DE73EA84-AE2A-4060-AA59-0EE409845232}"
EndProject
Expand Down
3 changes: 2 additions & 1 deletion Docker.Registry.DotNet.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue"> Copyright 2017-$CURRENT_YEAR$ Rich Quackenbush, Jaben Cargman&#xD;
<s:String x:Key="/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue"> Copyright 2017-${CurrentDate.Year} Rich Quackenbush, Jaben Cargman&#xD;
and Docker.Registry.DotNet Contributors&#xD;
&#xD;
Licensed under the Apache License, Version 2.0 (the "License");&#xD;
Expand All @@ -13,6 +13,7 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#xD;
See the License for the specific language governing permissions and&#xD;
limitations under the License.</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002ECodeCleanup_002EFileHeader_002EFileHeaderSettingsMigrate/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Cargman/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Jaben/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Quackenbush/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
45 changes: 38 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,49 @@ dotnet add package Docker.Registry.DotNet
```

# Usage

### Local Hub

```csharp
var configuration = new RegistryClientConfiguration("localhost:5000");
var configuration = new RegistryClientConfiguration("http://localhost:5000");

//configuration.UsePasswordOAuthAuthentication("username", "password")

using (var client = configuration.CreateClient())
{
await client.System.PingAsync();
// get catalog
var catalog = await client.Catalog.GetCatalog();

// list tags for the first catalog
var tags = await client.Tags.ListTags(catalog?.Repositories.FirstOrDefault());
}
```

# Changelog
### Remote Hub with Authentication

```csharp
var configuration = new RegistryClientConfiguration("https://proget.mycompany.com");

configuration.UsePasswordOAuthAuthentication("username", "password")

using (var client = configuration.CreateClient())
{
// get catalog
var catalog = await client.Catalog.GetCatalog();

### v1.1.33
* Added Basic Authentication (thanks [Zguy](https://github.com/Zguy)).
* Fixed issue with operational parameters (thanks [lostllama](https://github.com/lostllama)).
* Fixed issue with large manifest layers (thanks [msvprogs](https://github.com/msvprogs)).
// list tags for the first catalog
var tags = await client.Tags.ListTags(catalog?.Repositories.FirstOrDefault());
}
```

### Docker Hub

```csharp
var configuration = new RegistryClientConfiguration("https://hub.docker.com");

using (var client = configuration.CreateClient())
{
// load respository
var tags = await client.Repository.ListRepositoryTags("grafana", "loki-docker-driver");
}
```
2 changes: 1 addition & 1 deletion samples/Docker.Registry.Cli/Docker.Registry.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
Expand Down
4 changes: 1 addition & 3 deletions samples/Docker.Registry.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
using System.Security.Cryptography;
using System.Threading.Tasks;
using Docker.Registry.DotNet;
using Docker.Registry.DotNet.Authentication;
using Docker.Registry.DotNet.Models;
using Docker.Registry.DotNet.Registry;
using Docker.Registry.DotNet.Domain.Registry;

namespace Docker.Registry.Cli
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ public void Refresh()

private async Task ListImagesTags()
{
var tags = await this._registryClient.Tags.ListImageTagsAsync(
var tags = await this._registryClient.Tags.ListTags(
this.Name,
new ListImageTagsParameters());
new ListTagsParameters());

if (tags.Tags == null) this.Tags = new TagViewModel[] { };
else
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman
// and Docker.Registry.DotNet Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Docker.Registry.DotNet.Application.OAuth;

namespace Docker.Registry.DotNet.Application.Authentication;

[PublicAPI]
public class AnonymousOAuthAuthenticationProvider : AuthenticationProvider
{
private readonly OAuthClient _client = new();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid initializing OAuthClient directly.

Consider using dependency injection to initialize _client. This approach enhances testability and flexibility.

- private readonly OAuthClient _client = new();
+ private readonly OAuthClient _client;

public AnonymousOAuthAuthenticationProvider(OAuthClient client)
{
    _client = client;
}
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private readonly OAuthClient _client = new();
private readonly OAuthClient _client;
public AnonymousOAuthAuthenticationProvider(OAuthClient client)
{
_client = client;
}


private static string Schema { get; } = "Bearer";

public override Task Authenticate(HttpRequestMessage request, IRegistryUriBuilder uriBuilder)
{
using var activity = Assembly.Source.StartActivity("AnonymousOAuthAuthenticationProvider.Authenticate(request)");

return Task.CompletedTask;
}

public override async Task Authenticate(
HttpRequestMessage request,
HttpResponseMessage response,
IRegistryUriBuilder uriBuilder)
{
using var activity = Assembly.Source.StartActivity("AnonymousOAuthAuthenticationProvider.Authenticate(request, response)");

var header = this.TryGetSchemaHeader(response, Schema);

//Get the bearer bits
var bearerBits = AuthenticateParser.ParseTyped(header.Parameter);

//Get the token
var token = await this._client.GetToken(
bearerBits.Realm,
bearerBits.Service,
bearerBits.Scope);

//Set the header
request.Headers.Authorization = new AuthenticationHeaderValue(Schema, token.Token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman
// and Docker.Registry.DotNet Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace Docker.Registry.DotNet.Application.Authentication;

internal static class AuthenticateParser
{
public static IDictionary<string, string> Parse(string value)
{
//https://stackoverflow.com/questions/45516717/extracting-and-parsing-the-www-authenticate-header-from-httpresponsemessage-in/45516809#45516809
return SplitWWWAuthenticateHeader(value).ToDictionary(GetKey, GetValue);
}

private static IEnumerable<string> SplitWWWAuthenticateHeader(string value)
{
var builder = new StringBuilder();
var inQuotes = false;
for (var i = 0; i < value.Length; i++)
{
var charI = value[i];
switch (charI)
{
case '\"':
if (inQuotes)
{
yield return builder.ToString();
builder.Clear();
inQuotes = false;
}
else
{
inQuotes = true;
}

break;

case ',':
if (inQuotes)
{
builder.Append(charI);
}
else
{
if (builder.Length > 0)
{
yield return builder.ToString();
builder.Clear();
}
}

break;

default:
builder.Append(charI);
break;
}
}

if (builder.Length > 0) yield return builder.ToString();
}
Comment on lines +26 to +72
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimize the parsing logic in SplitWWWAuthenticateHeader.

Consider using a more efficient parsing approach, such as regular expressions, to handle complex header formats and improve readability.

// Example: Consider using regular expressions for parsing.


public static ParsedAuthentication ParseTyped(string value)
{
var parsed = Parse(value);

return new ParsedAuthentication(
parsed.GetValueOrDefault("realm"),
parsed.GetValueOrDefault("service"),
parsed.GetValueOrDefault("scope"));
}

private static string GetKey(string pair)
{
var equalPos = pair.IndexOf("=", StringComparison.Ordinal);

if (equalPos < 1)
throw new FormatException("No '=' found.");

return pair.Substring(0, equalPos);
}

private static string GetValue(string pair)
{
var equalPos = pair.IndexOf("=", StringComparison.Ordinal);

if (equalPos < 1)
throw new FormatException("No '=' found.");

var value = pair.Substring(equalPos + 1).Trim();

//Trim quotes
if (value.StartsWith("\"") && value.EndsWith("\""))
value = value.Substring(1, value.Length - 2);

return value;
}
Comment on lines +84 to +108
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure robust error handling in GetKey and GetValue methods.

The methods throw FormatException for malformed input. Consider logging the input for better diagnostics and handling unexpected cases gracefully.

// Example: Log malformed input for diagnostics.
if (equalPos < 1)
{
    // Log the malformed pair for debugging purposes.
    throw new FormatException("No '=' found.");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman
// and Docker.Registry.DotNet Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace Docker.Registry.DotNet.Application.Authentication;

/// <summary>
/// Authentication provider.
/// </summary>
public abstract class AuthenticationProvider
{
/// <summary>
/// Called on initial connection
/// </summary>
/// <param name="request"></param>
/// <param name="uriBuilder"></param>
/// <returns></returns>
public abstract Task Authenticate(HttpRequestMessage request, IRegistryUriBuilder uriBuilder);

/// <summary>
/// Called when connection is challenged.
/// </summary>
/// <param name="request"></param>
/// <param name="response"></param>
/// <param name="builder"></param>
/// <returns></returns>
public abstract Task Authenticate(
HttpRequestMessage request,
HttpResponseMessage response,
IRegistryUriBuilder builder);

/// <summary>
/// Gets the schema header from the http response.
/// </summary>
/// <param name="response"></param>
/// <param name="schema"></param>
/// <returns></returns>
protected AuthenticationHeaderValue TryGetSchemaHeader(
HttpResponseMessage response,
string schema)
{
var header = response.GetHeaderBySchema(schema);

if (header == null)
throw new InvalidOperationException(
$"No WWW-Authenticate challenge was found for schema {schema}");

return header;
}
Comment on lines +49 to +60
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle potential null values gracefully.

The TryGetSchemaHeader method throws an InvalidOperationException if the header is null. Consider logging the exception or providing a more descriptive error message.

if (header == null)
{
    // Log the missing header scenario
    throw new InvalidOperationException(
        $"No WWW-Authenticate challenge was found for schema {schema}. Response: {response}");
}

}
Loading