@@ -21,8 +21,10 @@ import (
2121 "io"
2222 "net/http"
2323 "strings"
24+ "time"
2425
2526 authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
27+ "github.com/google/go-containerregistry/pkg/logs"
2628 "github.com/google/go-containerregistry/pkg/name"
2729)
2830
@@ -34,6 +36,9 @@ const (
3436 bearer challenge = "bearer"
3537)
3638
39+ // 300ms is the default fallback period for go's DNS dialer but we could make this configurable.
40+ var fallbackDelay = 300 * time .Millisecond
41+
3742type pingResp struct {
3843 challenge challenge
3944
@@ -49,82 +54,125 @@ func (c challenge) Canonical() challenge {
4954 return challenge (strings .ToLower (string (c )))
5055}
5156
52- func parseChallenge (suffix string ) map [string ]string {
53- kv := make (map [string ]string )
54- for _ , token := range strings .Split (suffix , "," ) {
55- // Trim any whitespace around each token.
56- token = strings .Trim (token , " " )
57-
58- // Break the token into a key/value pair
59- if parts := strings .SplitN (token , "=" , 2 ); len (parts ) == 2 {
60- // Unquote the value, if it is quoted.
61- kv [parts [0 ]] = strings .Trim (parts [1 ], `"` )
62- } else {
63- // If there was only one part, treat is as a key with an empty value
64- kv [token ] = ""
65- }
66- }
67- return kv
68- }
69-
7057func ping (ctx context.Context , reg name.Registry , t http.RoundTripper ) (* pingResp , error ) {
71- client := http.Client {Transport : t }
72-
7358 // This first attempts to use "https" for every request, falling back to http
7459 // if the registry matches our localhost heuristic or if it is intentionally
7560 // set to insecure via name.NewInsecureRegistry.
7661 schemes := []string {"https" }
7762 if reg .Scheme () == "http" {
7863 schemes = append (schemes , "http" )
7964 }
65+ if len (schemes ) == 1 {
66+ return pingSingle (ctx , reg , t , schemes [0 ])
67+ }
68+ return pingParallel (ctx , reg , t , schemes )
69+ }
8070
81- var errs []error
82- for _ , scheme := range schemes {
83- url := fmt .Sprintf ("%s://%s/v2/" , scheme , reg .Name ())
84- req , err := http .NewRequest (http .MethodGet , url , nil )
85- if err != nil {
86- return nil , err
87- }
88- resp , err := client .Do (req .WithContext (ctx ))
89- if err != nil {
90- errs = append (errs , err )
91- // Potentially retry with http.
92- continue
93- }
94- defer func () {
95- // By draining the body, make sure to reuse the connection made by
96- // the ping for the following access to the registry
97- io .Copy (io .Discard , resp .Body )
98- resp .Body .Close ()
99- }()
100-
101- switch resp .StatusCode {
102- case http .StatusOK :
103- // If we get a 200, then no authentication is needed.
71+ func pingSingle (ctx context.Context , reg name.Registry , t http.RoundTripper , scheme string ) (* pingResp , error ) {
72+ client := http.Client {Transport : t }
73+ url := fmt .Sprintf ("%s://%s/v2/" , scheme , reg .Name ())
74+ req , err := http .NewRequest (http .MethodGet , url , nil )
75+ if err != nil {
76+ return nil , err
77+ }
78+ resp , err := client .Do (req .WithContext (ctx ))
79+ if err != nil {
80+ return nil , err
81+ }
82+ defer func () {
83+ // By draining the body, make sure to reuse the connection made by
84+ // the ping for the following access to the registry
85+ io .Copy (io .Discard , resp .Body )
86+ resp .Body .Close ()
87+ }()
88+
89+ switch resp .StatusCode {
90+ case http .StatusOK :
91+ // If we get a 200, then no authentication is needed.
92+ return & pingResp {
93+ challenge : anonymous ,
94+ scheme : scheme ,
95+ }, nil
96+ case http .StatusUnauthorized :
97+ if challenges := authchallenge .ResponseChallenges (resp ); len (challenges ) != 0 {
98+ // If we hit more than one, let's try to find one that we know how to handle.
99+ wac := pickFromMultipleChallenges (challenges )
104100 return & pingResp {
105- challenge : anonymous ,
106- scheme : scheme ,
101+ challenge : challenge (wac .Scheme ).Canonical (),
102+ parameters : wac .Parameters ,
103+ scheme : scheme ,
107104 }, nil
108- case http .StatusUnauthorized :
109- if challenges := authchallenge .ResponseChallenges (resp ); len (challenges ) != 0 {
110- // If we hit more than one, let's try to find one that we know how to handle.
111- wac := pickFromMultipleChallenges (challenges )
112- return & pingResp {
113- challenge : challenge (wac .Scheme ).Canonical (),
114- parameters : wac .Parameters ,
115- scheme : scheme ,
116- }, nil
105+ }
106+ // Otherwise, just return the challenge without parameters.
107+ return & pingResp {
108+ challenge : challenge (resp .Header .Get ("WWW-Authenticate" )).Canonical (),
109+ scheme : scheme ,
110+ }, nil
111+ default :
112+ return nil , CheckError (resp , http .StatusOK , http .StatusUnauthorized )
113+ }
114+ }
115+
116+ // Based on the golang happy eyeballs dialParallel impl in net/dial.go.
117+ func pingParallel (ctx context.Context , reg name.Registry , t http.RoundTripper , schemes []string ) (* pingResp , error ) {
118+ returned := make (chan struct {})
119+ defer close (returned )
120+
121+ type pingResult struct {
122+ * pingResp
123+ error
124+ primary bool
125+ done bool
126+ }
127+
128+ results := make (chan pingResult )
129+
130+ startRacer := func (ctx context.Context , scheme string ) {
131+ pr , err := pingSingle (ctx , reg , t , scheme )
132+ select {
133+ case results <- pingResult {pingResp : pr , error : err , primary : scheme == "https" , done : true }:
134+ case <- returned :
135+ if pr != nil {
136+ logs .Debug .Printf ("%s lost race" , scheme )
137+ }
138+ }
139+ }
140+
141+ var primary , fallback pingResult
142+
143+ primaryCtx , primaryCancel := context .WithCancel (ctx )
144+ defer primaryCancel ()
145+ go startRacer (primaryCtx , schemes [0 ])
146+
147+ fallbackTimer := time .NewTimer (fallbackDelay )
148+ defer fallbackTimer .Stop ()
149+
150+ for {
151+ select {
152+ case <- fallbackTimer .C :
153+ fallbackCtx , fallbackCancel := context .WithCancel (ctx )
154+ defer fallbackCancel ()
155+ go startRacer (fallbackCtx , schemes [1 ])
156+
157+ case res := <- results :
158+ if res .error == nil {
159+ return res .pingResp , nil
160+ }
161+ if res .primary {
162+ primary = res
163+ } else {
164+ fallback = res
165+ }
166+ if primary .done && fallback .done {
167+ return nil , multierrs ([]error {primary .error , fallback .error })
168+ }
169+ if res .primary && fallbackTimer .Stop () {
170+ // Primary failed and we haven't started the fallback,
171+ // reset time to start fallback immediately.
172+ fallbackTimer .Reset (0 )
117173 }
118- // Otherwise, just return the challenge without parameters.
119- return & pingResp {
120- challenge : challenge (resp .Header .Get ("WWW-Authenticate" )).Canonical (),
121- scheme : scheme ,
122- }, nil
123- default :
124- return nil , CheckError (resp , http .StatusOK , http .StatusUnauthorized )
125174 }
126175 }
127- return nil , multierrs (errs )
128176}
129177
130178func pickFromMultipleChallenges (challenges []authchallenge.Challenge ) authchallenge.Challenge {
0 commit comments