...

Source file src/github.com/google/go-containerregistry/pkg/v1/remote/transport/ping.go

Documentation: github.com/google/go-containerregistry/pkg/v1/remote/transport

     1  // Copyright 2018 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package transport
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"net/http"
    23  	"strings"
    24  	"time"
    25  
    26  	authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
    27  	"github.com/google/go-containerregistry/pkg/logs"
    28  	"github.com/google/go-containerregistry/pkg/name"
    29  )
    30  
    31  // 300ms is the default fallback period for go's DNS dialer but we could make this configurable.
    32  var fallbackDelay = 300 * time.Millisecond
    33  
    34  type Challenge struct {
    35  	Scheme string
    36  
    37  	// Following the challenge there are often key/value pairs
    38  	// e.g. Bearer service="gcr.io",realm="https://auth.gcr.io/v36/tokenz"
    39  	Parameters map[string]string
    40  
    41  	// Whether we had to use http to complete the Ping.
    42  	Insecure bool
    43  }
    44  
    45  // Ping does a GET /v2/ against the registry and returns the response.
    46  func Ping(ctx context.Context, reg name.Registry, t http.RoundTripper) (*Challenge, error) {
    47  	// This first attempts to use "https" for every request, falling back to http
    48  	// if the registry matches our localhost heuristic or if it is intentionally
    49  	// set to insecure via name.NewInsecureRegistry.
    50  	schemes := []string{"https"}
    51  	if reg.Scheme() == "http" {
    52  		schemes = append(schemes, "http")
    53  	}
    54  	if len(schemes) == 1 {
    55  		return pingSingle(ctx, reg, t, schemes[0])
    56  	}
    57  	return pingParallel(ctx, reg, t, schemes)
    58  }
    59  
    60  func pingSingle(ctx context.Context, reg name.Registry, t http.RoundTripper, scheme string) (*Challenge, error) {
    61  	client := http.Client{Transport: t}
    62  	url := fmt.Sprintf("%s://%s/v2/", scheme, reg.RegistryStr())
    63  	req, err := http.NewRequest(http.MethodGet, url, nil)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	resp, err := client.Do(req.WithContext(ctx))
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  	defer func() {
    72  		// By draining the body, make sure to reuse the connection made by
    73  		// the ping for the following access to the registry
    74  		io.Copy(io.Discard, resp.Body)
    75  		resp.Body.Close()
    76  	}()
    77  
    78  	insecure := scheme == "http"
    79  
    80  	switch resp.StatusCode {
    81  	case http.StatusOK:
    82  		// If we get a 200, then no authentication is needed.
    83  		return &Challenge{
    84  			Insecure: insecure,
    85  		}, nil
    86  	case http.StatusUnauthorized:
    87  		if challenges := authchallenge.ResponseChallenges(resp); len(challenges) != 0 {
    88  			// If we hit more than one, let's try to find one that we know how to handle.
    89  			wac := pickFromMultipleChallenges(challenges)
    90  			return &Challenge{
    91  				Scheme:     wac.Scheme,
    92  				Parameters: wac.Parameters,
    93  				Insecure:   insecure,
    94  			}, nil
    95  		}
    96  		// Otherwise, just return the challenge without parameters.
    97  		return &Challenge{
    98  			Scheme:   resp.Header.Get("WWW-Authenticate"),
    99  			Insecure: insecure,
   100  		}, nil
   101  	default:
   102  		return nil, CheckError(resp, http.StatusOK, http.StatusUnauthorized)
   103  	}
   104  }
   105  
   106  // Based on the golang happy eyeballs dialParallel impl in net/dial.go.
   107  func pingParallel(ctx context.Context, reg name.Registry, t http.RoundTripper, schemes []string) (*Challenge, error) {
   108  	returned := make(chan struct{})
   109  	defer close(returned)
   110  
   111  	type pingResult struct {
   112  		*Challenge
   113  		error
   114  		primary bool
   115  		done    bool
   116  	}
   117  
   118  	results := make(chan pingResult)
   119  
   120  	startRacer := func(ctx context.Context, scheme string) {
   121  		pr, err := pingSingle(ctx, reg, t, scheme)
   122  		select {
   123  		case results <- pingResult{Challenge: pr, error: err, primary: scheme == "https", done: true}:
   124  		case <-returned:
   125  			if pr != nil {
   126  				logs.Debug.Printf("%s lost race", scheme)
   127  			}
   128  		}
   129  	}
   130  
   131  	var primary, fallback pingResult
   132  
   133  	primaryCtx, primaryCancel := context.WithCancel(ctx)
   134  	defer primaryCancel()
   135  	go startRacer(primaryCtx, schemes[0])
   136  
   137  	fallbackTimer := time.NewTimer(fallbackDelay)
   138  	defer fallbackTimer.Stop()
   139  
   140  	for {
   141  		select {
   142  		case <-fallbackTimer.C:
   143  			fallbackCtx, fallbackCancel := context.WithCancel(ctx)
   144  			defer fallbackCancel()
   145  			go startRacer(fallbackCtx, schemes[1])
   146  
   147  		case res := <-results:
   148  			if res.error == nil {
   149  				return res.Challenge, nil
   150  			}
   151  			if res.primary {
   152  				primary = res
   153  			} else {
   154  				fallback = res
   155  			}
   156  			if primary.done && fallback.done {
   157  				return nil, multierrs{primary.error, fallback.error}
   158  			}
   159  			if res.primary && fallbackTimer.Stop() {
   160  				// Primary failed and we haven't started the fallback,
   161  				// reset time to start fallback immediately.
   162  				fallbackTimer.Reset(0)
   163  			}
   164  		}
   165  	}
   166  }
   167  
   168  func pickFromMultipleChallenges(challenges []authchallenge.Challenge) authchallenge.Challenge {
   169  	// It might happen there are multiple www-authenticate headers, e.g. `Negotiate` and `Basic`.
   170  	// Picking simply the first one could result eventually in `unrecognized challenge` error,
   171  	// that's why we're looping through the challenges in search for one that can be handled.
   172  	allowedSchemes := []string{"basic", "bearer"}
   173  
   174  	for _, wac := range challenges {
   175  		currentScheme := strings.ToLower(wac.Scheme)
   176  		for _, allowed := range allowedSchemes {
   177  			if allowed == currentScheme {
   178  				return wac
   179  			}
   180  		}
   181  	}
   182  
   183  	return challenges[0]
   184  }
   185  
   186  type multierrs []error
   187  
   188  func (m multierrs) Error() string {
   189  	var b strings.Builder
   190  	hasWritten := false
   191  	for _, err := range m {
   192  		if hasWritten {
   193  			b.WriteString("; ")
   194  		}
   195  		hasWritten = true
   196  		b.WriteString(err.Error())
   197  	}
   198  	return b.String()
   199  }
   200  
   201  func (m multierrs) As(target any) bool {
   202  	for _, err := range m {
   203  		if errors.As(err, target) {
   204  			return true
   205  		}
   206  	}
   207  	return false
   208  }
   209  
   210  func (m multierrs) Is(target error) bool {
   211  	for _, err := range m {
   212  		if errors.Is(err, target) {
   213  			return true
   214  		}
   215  	}
   216  	return false
   217  }
   218  

View as plain text