Skip to content

Commit 19a36cd

Browse files
authored
fork distribution client v3 auth-challenge as an internal package (squashed) (#2248)
* fork distribution client v3 auth-challenge as an internal package This forks the distribution's registry client auth-challenge code v3.0.0; distribution/distribution@9ed95e7 The client code has moved to an internal package in the distribution project, and is used to handle `WWW-Authenticate` headers. It was written a long time ago, and only handles RFC 2617 (superseded by RFC 9110). Possibly there's more current implementations for this, but this would introduce new dependencies, so we can keep the status quo for now, and fork this code as an internal package. Keeping it internal, due to the known limitations mentioned above, and to avoid it being used by others assuming it's a general-purpose implementation. This is the result of the following steps: # install filter-repo (https://github.com/newren/git-filter-repo/blob/main/INSTALL.md) brew install git-filter-repo # create a temporary clone of docker cd ~/Projects git clone https://github.com/distribution/distribution.git distribution_auth cd distribution_auth # switch to v3.0.0 (git-filter doesn't like multiple branches, so reset) git reset --hard v3.0.0 # commit taken from git rev-parse --verify HEAD 9ed95e7365224025ee89365e12cf128e1f1bf965 git filter-repo --analyze # remove all code, except for 'internal/client/auth/challenge' # rename to 'pkg/v1/remote/internal/authchallenge' # include history of old location ('registry/client/auth/challenge') git filter-repo \ --force \ --path 'internal/client/auth/challenge' \ --path 'registry/client/auth/challenge' \ --path-rename internal/client/auth/challenge:pkg/v1/remote/internal/authchallenge # go to the target github.com/docker/docker repository cd ~/go/src/github.com/google/go-containerregistry # create a branch to work with git checkout -b fork_registryclient_authchallenge # add the temporary repository as an upstream and make sure it's up-to-date git remote add distribution_auth ~/Projects/distribution_auth git fetch distribution_auth # merge the upstream code git merge --allow-unrelated-histories --signoff -S distribution_auth/main History was squashed: - Refactor authorization challenges to its own package Split challenges into its own package. Avoids possible import cycle with challenges from client. - Fix minor "Challanges" typo - Fix gometalint errors - Fix typo in NewSimpleManager() documentation - Initial repo commit - init - format code with gofumpt Move registry client internal Our registry client is not currently in a good place to be used as the reference OCI Distribution client implementation. But the registry proxy currently depends on it. Make the registry client internal to the distribution application to remove it from the API surface area (and any implied compatibility promises) of distribution/v3@v3.0.0 without breaking the proxy. cleanup: move init funcs to the top of the source We make sure they're not hiding at the bottom or in the middle which makes debugging an utter nightmare! Signed-off-by: Sebastiaan van Stijn <github@gone.nl> * authchallenge: rename package and update imports Signed-off-by: Sebastiaan van Stijn <github@gone.nl> * pkg/v1/remote/internal/authchallenge: make manager-code a test-util The Manager code is only used as part of tests; move it to _test files to have a clearer separation between "production" code and test-code. Signed-off-by: Sebastiaan van Stijn <github@gone.nl> * pkg/v1/remote/internal/authchallenge: cleanup manager code As this code is now only used as a test-utility; - Make the zero-value of simpleManager usable - Remove the constructor and interface definition - Un-export fields and mutex. Signed-off-by: Sebastiaan van Stijn <github@gone.nl> * pkg/v1/remote/internal/authchallenge: cleanup URL-normalization - change normalizeURL to not mutate the input URL. - move lowercasing into canonicalAddr. Signed-off-by: Sebastiaan van Stijn <github@gone.nl> * pkg/v1/remote/internal/authchallenge: modernize and linting Signed-off-by: Sebastiaan van Stijn <github@gone.nl> * pkg/v1/remote/internal/authchallenge: add boilerplating Signed-off-by: Sebastiaan van Stijn <github@gone.nl> --------- Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
1 parent c612a9b commit 19a36cd

14 files changed

Lines changed: 257 additions & 327 deletions

File tree

cmd/krane/go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ require (
4545
github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
4646
github.com/dimchansky/utfbom v1.1.1 // indirect
4747
github.com/docker/cli v29.3.1+incompatible // indirect
48-
github.com/docker/distribution v2.8.3+incompatible // indirect
4948
github.com/docker/docker-credential-helpers v0.9.5 // indirect
5049
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
5150
github.com/google/go-cmp v0.7.0 // indirect

cmd/krane/go.sum

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ go 1.25.0
1010
require (
1111
github.com/containerd/stargz-snapshotter/estargz v0.18.2
1212
github.com/docker/cli v29.3.1+incompatible
13-
github.com/docker/distribution v2.8.3+incompatible
1413
github.com/google/go-cmp v0.7.0
1514
github.com/klauspost/compress v1.18.5
1615
github.com/mitchellh/go-homedir v1.1.0

go.sum

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/authn/k8schain/go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ require (
5252
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
5353
github.com/dimchansky/utfbom v1.1.1 // indirect
5454
github.com/docker/cli v29.3.1+incompatible // indirect
55-
github.com/docker/distribution v2.8.3+incompatible // indirect
5655
github.com/docker/docker-credential-helpers v0.9.5 // indirect
5756
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
5857
github.com/fxamacker/cbor/v2 v2.9.0 // indirect

pkg/authn/k8schain/go.sum

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/docker/distribution/registry/client/auth/challenge/authchallenge.go renamed to pkg/v1/remote/internal/authchallenge/authchallenge.go

Lines changed: 28 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,26 @@
1-
package challenge
1+
// Copyright 2014 Docker, Inc.
2+
// Copyright 2021-2026 The Distribution contributors
3+
// Copyright 2026 Google LLC All Rights Reserved.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
package authchallenge
218

319
import (
4-
"fmt"
520
"net/http"
6-
"net/url"
721
"strings"
8-
"sync"
922
)
1023

11-
// Challenge carries information from a WWW-Authenticate response header.
12-
// See RFC 2617.
13-
type Challenge struct {
14-
// Scheme is the auth-scheme according to RFC 2617
15-
Scheme string
16-
17-
// Parameters are the auth-params according to RFC 2617
18-
Parameters map[string]string
19-
}
20-
21-
// Manager manages the challenges for endpoints.
22-
// The challenges are pulled out of HTTP responses. Only
23-
// responses which expect challenges should be added to
24-
// the manager, since a non-unauthorized request will be
25-
// viewed as not requiring challenges.
26-
type Manager interface {
27-
// GetChallenges returns the challenges for the given
28-
// endpoint URL.
29-
GetChallenges(endpoint url.URL) ([]Challenge, error)
30-
31-
// AddResponse adds the response to the challenge
32-
// manager. The challenges will be parsed out of
33-
// the WWW-Authenicate headers and added to the
34-
// URL which was produced the response. If the
35-
// response was authorized, any challenges for the
36-
// endpoint will be cleared.
37-
AddResponse(resp *http.Response) error
38-
}
39-
40-
// NewSimpleManager returns an instance of
41-
// Manger which only maps endpoints to challenges
42-
// based on the responses which have been added the
43-
// manager. The simple manager will make no attempt to
44-
// perform requests on the endpoints or cache the responses
45-
// to a backend.
46-
func NewSimpleManager() Manager {
47-
return &simpleManager{
48-
Challenges: make(map[string][]Challenge),
49-
}
50-
}
51-
52-
type simpleManager struct {
53-
sync.RWMutex
54-
Challenges map[string][]Challenge
55-
}
56-
57-
func normalizeURL(endpoint *url.URL) {
58-
endpoint.Host = strings.ToLower(endpoint.Host)
59-
endpoint.Host = canonicalAddr(endpoint)
60-
}
61-
62-
func (m *simpleManager) GetChallenges(endpoint url.URL) ([]Challenge, error) {
63-
normalizeURL(&endpoint)
64-
65-
m.RLock()
66-
defer m.RUnlock()
67-
challenges := m.Challenges[endpoint.String()]
68-
return challenges, nil
69-
}
70-
71-
func (m *simpleManager) AddResponse(resp *http.Response) error {
72-
challenges := ResponseChallenges(resp)
73-
if resp.Request == nil {
74-
return fmt.Errorf("missing request reference")
75-
}
76-
urlCopy := url.URL{
77-
Path: resp.Request.URL.Path,
78-
Host: resp.Request.URL.Host,
79-
Scheme: resp.Request.URL.Scheme,
80-
}
81-
normalizeURL(&urlCopy)
82-
83-
m.Lock()
84-
defer m.Unlock()
85-
m.Challenges[urlCopy.String()] = challenges
86-
return nil
87-
}
88-
8924
// Octet types from RFC 2616.
9025
type octetType byte
9126

@@ -113,7 +48,7 @@ func init() {
11348
// token = 1*<any CHAR except CTLs or separators>
11449
// qdtext = <any TEXT except <">>
11550

116-
for c := 0; c < 256; c++ {
51+
for c := range 256 {
11752
var t octetType
11853
isCtl := c <= 31 || c == 127
11954
isChar := 0 <= c && c <= 127
@@ -128,6 +63,16 @@ func init() {
12863
}
12964
}
13065

66+
// Challenge carries information from a WWW-Authenticate response header.
67+
// See RFC 2617.
68+
type Challenge struct {
69+
// Scheme is the auth-scheme according to RFC 2617
70+
Scheme string
71+
72+
// Parameters are the auth-params according to RFC 2617
73+
Parameters map[string]string
74+
}
75+
13176
// ResponseChallenges returns a list of authorization challenges
13277
// for the given http Response. Challenges are only checked if
13378
// the response status code was a 401.
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright 2014 Docker, Inc.
2+
// Copyright 2021-2026 The Distribution contributors
3+
// Copyright 2026 Google LLC All Rights Reserved.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
package authchallenge
18+
19+
import (
20+
"fmt"
21+
"net/http"
22+
"net/url"
23+
"strings"
24+
"sync"
25+
"testing"
26+
)
27+
28+
func TestAuthChallengeParse(t *testing.T) {
29+
header := http.Header{}
30+
header.Add("WWW-Authenticate", `Bearer realm="https://auth.example.com/token",service="registry.example.com",other=fun,slashed="he\"\l\lo"`)
31+
32+
challenges := parseAuthHeader(header)
33+
if len(challenges) != 1 {
34+
t.Fatalf("Unexpected number of auth challenges: %d, expected 1", len(challenges))
35+
}
36+
challenge := challenges[0]
37+
38+
if expected := "bearer"; challenge.Scheme != expected {
39+
t.Fatalf("Unexpected scheme: %s, expected: %s", challenge.Scheme, expected)
40+
}
41+
42+
if expected := "https://auth.example.com/token"; challenge.Parameters["realm"] != expected {
43+
t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["realm"], expected)
44+
}
45+
46+
if expected := "registry.example.com"; challenge.Parameters["service"] != expected {
47+
t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["service"], expected)
48+
}
49+
50+
if expected := "fun"; challenge.Parameters["other"] != expected {
51+
t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["other"], expected)
52+
}
53+
54+
if expected := "he\"llo"; challenge.Parameters["slashed"] != expected {
55+
t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["slashed"], expected)
56+
}
57+
}
58+
59+
func TestAuthChallengeNormalization(t *testing.T) {
60+
testAuthChallengeNormalization(t, "reg.EXAMPLE.com")
61+
testAuthChallengeNormalization(t, "bɿɒʜɔiɿ-ɿɘƚƨim-ƚol-ɒ-ƨʞnɒʜƚ.com")
62+
testAuthChallengeNormalization(t, "reg.example.com:80")
63+
testAuthChallengeConcurrent(t, "reg.EXAMPLE.com")
64+
}
65+
66+
func testAuthChallengeNormalization(t *testing.T, host string) {
67+
scm := &simpleManager{}
68+
69+
hostURL, err := url.Parse(fmt.Sprintf("https://%s/v2/", host))
70+
if err != nil {
71+
t.Fatal(err)
72+
}
73+
74+
resp := &http.Response{
75+
Request: &http.Request{
76+
URL: hostURL,
77+
},
78+
Header: make(http.Header),
79+
StatusCode: http.StatusUnauthorized,
80+
}
81+
resp.Header.Add("WWW-Authenticate", fmt.Sprintf(`Bearer realm="https://%s/token",service="registry.example.com"`, host))
82+
83+
err = scm.AddResponse(resp)
84+
if err != nil {
85+
t.Fatal(err)
86+
}
87+
88+
lowered := *hostURL
89+
lowered.Host = canonicalAddr(&lowered)
90+
c, err := scm.GetChallenges(lowered)
91+
if err != nil {
92+
t.Fatal(err)
93+
}
94+
95+
if len(c) == 0 {
96+
t.Fatal("Expected challenge for lower-cased-host URL")
97+
}
98+
}
99+
100+
func testAuthChallengeConcurrent(t *testing.T, host string) {
101+
scm := &simpleManager{}
102+
103+
hostURL, err := url.Parse(fmt.Sprintf("https://%s/v2/", host))
104+
if err != nil {
105+
t.Fatal(err)
106+
}
107+
108+
resp := &http.Response{
109+
Request: &http.Request{
110+
URL: hostURL,
111+
},
112+
Header: make(http.Header),
113+
StatusCode: http.StatusUnauthorized,
114+
}
115+
resp.Header.Add("WWW-Authenticate", fmt.Sprintf(`Bearer realm="https://%s/token",service="registry.example.com"`, host))
116+
var s sync.WaitGroup
117+
s.Add(2)
118+
go func() {
119+
defer s.Done()
120+
for range 200 {
121+
err = scm.AddResponse(resp)
122+
if err != nil {
123+
t.Error(err)
124+
}
125+
}
126+
}()
127+
go func() {
128+
defer s.Done()
129+
lowered := *hostURL
130+
lowered.Host = strings.ToLower(lowered.Host)
131+
for range 200 {
132+
_, err := scm.GetChallenges(lowered)
133+
if err != nil {
134+
t.Error(err)
135+
}
136+
}
137+
}()
138+
s.Wait()
139+
}

0 commit comments

Comments
 (0)