Skip to content

Commit

Permalink
Inline dependency on (Apache licensed) auth challenge
Browse files Browse the repository at this point in the history
This code was made internal in distribution/distribution#4126,
because it was not intended to be consumed externally.

As it is Apache-licensed, start by inlining the dependency; we can
then enhance test coverage and address any shortcomings.
  • Loading branch information
justinsb committed Apr 1, 2024
1 parent 8b3c303 commit 5445c5f
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 121 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ go 1.18
require (
github.com/containerd/stargz-snapshotter/estargz v0.14.3
github.com/docker/cli v24.0.0+incompatible
github.com/docker/distribution v2.8.2+incompatible
github.com/docker/docker v24.0.0+incompatible
github.com/google/go-cmp v0.5.9
github.com/klauspost/compress v1.16.5
Expand All @@ -23,6 +22,7 @@ require (
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
Expand Down
3 changes: 1 addition & 2 deletions pkg/v1/remote/transport/bearer.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"net/url"
"strings"

authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
"github.com/google/go-containerregistry/internal/redact"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/logs"
Expand Down Expand Up @@ -151,7 +150,7 @@ func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) {
}

// If we hit a WWW-Authenticate challenge, it might be due to expired tokens or insufficient scope.
if challenges := authchallenge.ResponseChallenges(res); len(challenges) != 0 {
if challenges := authResponseChallenges(res); len(challenges) != 0 {
// close out old response, since we will not return it.
res.Body.Close()

Expand Down
5 changes: 2 additions & 3 deletions pkg/v1/remote/transport/ping.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"strings"
"time"

authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
"github.com/google/go-containerregistry/pkg/logs"
"github.com/google/go-containerregistry/pkg/name"
)
Expand Down Expand Up @@ -84,7 +83,7 @@ func pingSingle(ctx context.Context, reg name.Registry, t http.RoundTripper, sch
Insecure: insecure,
}, nil
case http.StatusUnauthorized:
if challenges := authchallenge.ResponseChallenges(resp); len(challenges) != 0 {
if challenges := authResponseChallenges(resp); len(challenges) != 0 {
// If we hit more than one, let's try to find one that we know how to handle.
wac := pickFromMultipleChallenges(challenges)
return &Challenge{
Expand Down Expand Up @@ -165,7 +164,7 @@ func pingParallel(ctx context.Context, reg name.Registry, t http.RoundTripper, s
}
}

func pickFromMultipleChallenges(challenges []authchallenge.Challenge) authchallenge.Challenge {
func pickFromMultipleChallenges(challenges []authChallenge) authChallenge {
// It might happen there are multiple www-authenticate headers, e.g. `Negotiate` and `Basic`.
// Picking simply the first one could result eventually in `unrecognized challenge` error,
// that's why we're looping through the challenges in search for one that can be handled.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,91 +1,28 @@
package challenge
// Copyright 2024 Google LLC All Rights Reserved.
//
// 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.

package transport

// This code is copy-paste imported from the Apache-licensed https://github.com/distribution/distribution,
// as the dependency has been made internal upstream.
// There is an alternative implementation in https://fuchsia.googlesource.com/tools/+/efc566f8f0dcc061dac3d57989b24f496b109ecb/net/digest/digest.go

import (
"fmt"
"net/http"
"net/url"
"strings"
"sync"
)

// Challenge carries information from a WWW-Authenticate response header.
// See RFC 2617.
type Challenge struct {
// Scheme is the auth-scheme according to RFC 2617
Scheme string

// Parameters are the auth-params according to RFC 2617
Parameters map[string]string
}

// Manager manages the challenges for endpoints.
// The challenges are pulled out of HTTP responses. Only
// responses which expect challenges should be added to
// the manager, since a non-unauthorized request will be
// viewed as not requiring challenges.
type Manager interface {
// GetChallenges returns the challenges for the given
// endpoint URL.
GetChallenges(endpoint url.URL) ([]Challenge, error)

// AddResponse adds the response to the challenge
// manager. The challenges will be parsed out of
// the WWW-Authenicate headers and added to the
// URL which was produced the response. If the
// response was authorized, any challenges for the
// endpoint will be cleared.
AddResponse(resp *http.Response) error
}

// NewSimpleManager returns an instance of
// Manger which only maps endpoints to challenges
// based on the responses which have been added the
// manager. The simple manager will make no attempt to
// perform requests on the endpoints or cache the responses
// to a backend.
func NewSimpleManager() Manager {
return &simpleManager{
Challenges: make(map[string][]Challenge),
}
}

type simpleManager struct {
sync.RWMutex
Challenges map[string][]Challenge
}

func normalizeURL(endpoint *url.URL) {
endpoint.Host = strings.ToLower(endpoint.Host)
endpoint.Host = canonicalAddr(endpoint)
}

func (m *simpleManager) GetChallenges(endpoint url.URL) ([]Challenge, error) {
normalizeURL(&endpoint)

m.RLock()
defer m.RUnlock()
challenges := m.Challenges[endpoint.String()]
return challenges, nil
}

func (m *simpleManager) AddResponse(resp *http.Response) error {
challenges := ResponseChallenges(resp)
if resp.Request == nil {
return fmt.Errorf("missing request reference")
}
urlCopy := url.URL{
Path: resp.Request.URL.Path,
Host: resp.Request.URL.Host,
Scheme: resp.Request.URL.Scheme,
}
normalizeURL(&urlCopy)

m.Lock()
defer m.Unlock()
m.Challenges[urlCopy.String()] = challenges
return nil
}

// Octet types from RFC 2616.
type octetType byte

Expand Down Expand Up @@ -128,10 +65,10 @@ func init() {
}
}

// ResponseChallenges returns a list of authorization challenges
// authResponseChallenges returns a list of authorization challenges
// for the given http Response. Challenges are only checked if
// the response status code was a 401.
func ResponseChallenges(resp *http.Response) []Challenge {
func authResponseChallenges(resp *http.Response) []authChallenge {
if resp.StatusCode == http.StatusUnauthorized {
// Parse the WWW-Authenticate Header and store the challenges
// on this endpoint object.
Expand All @@ -141,17 +78,26 @@ func ResponseChallenges(resp *http.Response) []Challenge {
return nil
}

func parseAuthHeader(header http.Header) []Challenge {
challenges := []Challenge{}
func parseAuthHeader(header http.Header) []authChallenge {
challenges := []authChallenge{}
for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] {
v, p := parseValueAndParams(h)
if v != "" {
challenges = append(challenges, Challenge{Scheme: v, Parameters: p})
challenges = append(challenges, authChallenge{Scheme: v, Parameters: p})
}
}
return challenges
}

// Note: we may be able to combine with Challenge here
type authChallenge struct {
Scheme string

// Following the challenge there are often key/value pairs
// e.g. Bearer service="gcr.io",realm="https://auth.gcr.io/v36/tokenz"
Parameters map[string]string
}

func parseValueAndParams(header string) (value string, params map[string]string) {
params = make(map[string]string)
value, s := expectToken(header)
Expand Down
54 changes: 54 additions & 0 deletions pkg/v1/remote/transport/response_challenge_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2024 Google LLC All Rights Reserved.
//
// 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.

package transport

// This code is copy-paste imported from the Apache-licensed https://github.com/distribution/distribution,
// as the dependency has been made internal upstream.

import (
"net/http"
"testing"
)

func TestAuthChallengeParse(t *testing.T) {
header := http.Header{}
header.Add("WWW-Authenticate", `Bearer realm="https://auth.example.com/token",service="registry.example.com",other=fun,slashed="he\"\l\lo"`)

challenges := parseAuthHeader(header)
if len(challenges) != 1 {
t.Fatalf("Unexpected number of auth challenges: %d, expected 1", len(challenges))
}
challenge := challenges[0]

if expected := "bearer"; challenge.Scheme != expected {
t.Fatalf("Unexpected scheme: %s, expected: %s", challenge.Scheme, expected)
}

if expected := "https://auth.example.com/token"; challenge.Parameters["realm"] != expected {
t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["realm"], expected)
}

if expected := "registry.example.com"; challenge.Parameters["service"] != expected {
t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["service"], expected)
}

if expected := "fun"; challenge.Parameters["other"] != expected {
t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["other"], expected)
}

if expected := "he\"llo"; challenge.Parameters["slashed"] != expected {
t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["slashed"], expected)
}
}

This file was deleted.

1 change: 0 additions & 1 deletion vendor/modules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ github.com/docker/cli/cli/config/types
## explicit
github.com/docker/distribution/digestset
github.com/docker/distribution/reference
github.com/docker/distribution/registry/client/auth/challenge
# github.com/docker/docker v24.0.0+incompatible
## explicit
github.com/docker/docker/api
Expand Down

0 comments on commit 5445c5f

Please sign in to comment.