forked from moby/moby
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathendpoint.go
276 lines (229 loc) · 8.23 KB
/
endpoint.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
package registry
import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/client/transport"
)
// for mocking in unit tests
var lookupIP = net.LookupIP
// scans string for api version in the URL path. returns the trimmed address, if version found, string and API version.
func scanForAPIVersion(address string) (string, APIVersion) {
var (
chunks []string
apiVersionStr string
)
if strings.HasSuffix(address, "/") {
address = address[:len(address)-1]
}
chunks = strings.Split(address, "/")
apiVersionStr = chunks[len(chunks)-1]
for k, v := range apiVersions {
if apiVersionStr == v {
address = strings.Join(chunks[:len(chunks)-1], "/")
return address, k
}
}
return address, APIVersionUnknown
}
// NewEndpoint parses the given address to return a registry endpoint. v can be used to
// specify a specific endpoint version
func NewEndpoint(index *IndexInfo, metaHeaders http.Header, v APIVersion) (*Endpoint, error) {
tlsConfig, err := newTLSConfig(index.Name, index.Secure)
if err != nil {
return nil, err
}
endpoint, err := newEndpoint(index.GetAuthConfigKey(), tlsConfig, metaHeaders)
if err != nil {
return nil, err
}
if v != APIVersionUnknown {
endpoint.Version = v
}
if err := validateEndpoint(endpoint); err != nil {
return nil, err
}
return endpoint, nil
}
func validateEndpoint(endpoint *Endpoint) error {
logrus.Debugf("pinging registry endpoint %s", endpoint)
// Try HTTPS ping to registry
endpoint.URL.Scheme = "https"
if _, err := endpoint.Ping(); err != nil {
if endpoint.IsSecure {
// If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry`
// in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP.
return fmt.Errorf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host)
}
// If registry is insecure and HTTPS failed, fallback to HTTP.
logrus.Debugf("Error from registry %q marked as insecure: %v. Insecurely falling back to HTTP", endpoint, err)
endpoint.URL.Scheme = "http"
var err2 error
if _, err2 = endpoint.Ping(); err2 == nil {
return nil
}
return fmt.Errorf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
}
return nil
}
func newEndpoint(address string, tlsConfig *tls.Config, metaHeaders http.Header) (*Endpoint, error) {
var (
endpoint = new(Endpoint)
trimmedAddress string
err error
)
if !strings.HasPrefix(address, "http") {
address = "https://" + address
}
endpoint.IsSecure = (tlsConfig == nil || !tlsConfig.InsecureSkipVerify)
trimmedAddress, endpoint.Version = scanForAPIVersion(address)
if endpoint.URL, err = url.Parse(trimmedAddress); err != nil {
return nil, err
}
// TODO(tiborvass): make sure a ConnectTimeout transport is used
tr := NewTransport(tlsConfig)
endpoint.client = HTTPClient(transport.NewTransport(tr, DockerHeaders(metaHeaders)...))
return endpoint, nil
}
// Endpoint stores basic information about a registry endpoint.
type Endpoint struct {
client *http.Client
URL *url.URL
Version APIVersion
IsSecure bool
AuthChallenges []*AuthorizationChallenge
URLBuilder *v2.URLBuilder
}
// Get the formated URL for the root of this registry Endpoint
func (e *Endpoint) String() string {
return fmt.Sprintf("%s/v%d/", e.URL, e.Version)
}
// VersionString returns a formatted string of this
// endpoint address using the given API Version.
func (e *Endpoint) VersionString(version APIVersion) string {
return fmt.Sprintf("%s/v%d/", e.URL, version)
}
// Path returns a formatted string for the URL
// of this endpoint with the given path appended.
func (e *Endpoint) Path(path string) string {
return fmt.Sprintf("%s/v%d/%s", e.URL, e.Version, path)
}
// Ping pings the remote endpoint with v2 and v1 pings to determine the API
// version. It returns a PingResult containing the discovered version. The
// PingResult also indicates whether the registry is standalone or not.
func (e *Endpoint) Ping() (PingResult, error) {
// The ping logic to use is determined by the registry endpoint version.
switch e.Version {
case APIVersion1:
return e.pingV1()
case APIVersion2:
return e.pingV2()
}
// APIVersionUnknown
// We should try v2 first...
e.Version = APIVersion2
regInfo, errV2 := e.pingV2()
if errV2 == nil {
return regInfo, nil
}
// ... then fallback to v1.
e.Version = APIVersion1
regInfo, errV1 := e.pingV1()
if errV1 == nil {
return regInfo, nil
}
e.Version = APIVersionUnknown
return PingResult{}, fmt.Errorf("unable to ping registry endpoint %s\nv2 ping attempt failed with error: %s\n v1 ping attempt failed with error: %s", e, errV2, errV1)
}
func (e *Endpoint) pingV1() (PingResult, error) {
logrus.Debugf("attempting v1 ping for registry endpoint %s", e)
if e.String() == IndexServer {
// Skip the check, we know this one is valid
// (and we never want to fallback to http in case of error)
return PingResult{Standalone: false}, nil
}
req, err := http.NewRequest("GET", e.Path("_ping"), nil)
if err != nil {
return PingResult{Standalone: false}, err
}
resp, err := e.client.Do(req)
if err != nil {
return PingResult{Standalone: false}, err
}
defer resp.Body.Close()
jsonString, err := ioutil.ReadAll(resp.Body)
if err != nil {
return PingResult{Standalone: false}, fmt.Errorf("error while reading the http response: %s", err)
}
// If the header is absent, we assume true for compatibility with earlier
// versions of the registry. default to true
info := PingResult{
Standalone: true,
}
if err := json.Unmarshal(jsonString, &info); err != nil {
logrus.Debugf("Error unmarshalling the _ping PingResult: %s", err)
// don't stop here. Just assume sane defaults
}
if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" {
logrus.Debugf("Registry version header: '%s'", hdr)
info.Version = hdr
}
logrus.Debugf("PingResult.Version: %q", info.Version)
standalone := resp.Header.Get("X-Docker-Registry-Standalone")
logrus.Debugf("Registry standalone header: '%s'", standalone)
// Accepted values are "true" (case-insensitive) and "1".
if strings.EqualFold(standalone, "true") || standalone == "1" {
info.Standalone = true
} else if len(standalone) > 0 {
// there is a header set, and it is not "true" or "1", so assume fails
info.Standalone = false
}
logrus.Debugf("PingResult.Standalone: %t", info.Standalone)
return info, nil
}
func (e *Endpoint) pingV2() (PingResult, error) {
logrus.Debugf("attempting v2 ping for registry endpoint %s", e)
req, err := http.NewRequest("GET", e.Path(""), nil)
if err != nil {
return PingResult{}, err
}
resp, err := e.client.Do(req)
if err != nil {
return PingResult{}, err
}
defer resp.Body.Close()
// The endpoint may have multiple supported versions.
// Ensure it supports the v2 Registry API.
var supportsV2 bool
HeaderLoop:
for _, supportedVersions := range resp.Header[http.CanonicalHeaderKey("Docker-Distribution-API-Version")] {
for _, versionName := range strings.Fields(supportedVersions) {
if versionName == "registry/2.0" {
supportsV2 = true
break HeaderLoop
}
}
}
if !supportsV2 {
return PingResult{}, fmt.Errorf("%s does not appear to be a v2 registry endpoint", e)
}
if resp.StatusCode == http.StatusOK {
// It would seem that no authentication/authorization is required.
// So we don't need to parse/add any authorization schemes.
return PingResult{Standalone: true}, nil
}
if resp.StatusCode == http.StatusUnauthorized {
// Parse the WWW-Authenticate Header and store the challenges
// on this endpoint object.
e.AuthChallenges = parseAuthHeader(resp.Header)
return PingResult{}, nil
}
return PingResult{}, fmt.Errorf("v2 registry endpoint returned status %d: %q", resp.StatusCode, http.StatusText(resp.StatusCode))
}