summaryrefslogtreecommitdiff
path: root/registry/endpoint_v1.go
blob: 56257dc799af7fca10277458079f8b4989ead033 (plain)
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
package registry // import "github.com/docker/docker/registry"

import (
	"crypto/tls"
	"encoding/json"
	"io"
	"net/http"
	"net/url"
	"strings"

	"github.com/docker/distribution/registry/client/transport"
	"github.com/docker/docker/api/types/registry"
	"github.com/sirupsen/logrus"
)

// v1PingResult contains the information returned when pinging a registry. It
// indicates the registry's version and whether the registry claims to be a
// standalone registry.
type v1PingResult struct {
	// Version is the registry version supplied by the registry in an HTTP
	// header
	Version string `json:"version"`
	// Standalone is set to true if the registry indicates it is a
	// standalone registry in the X-Docker-Registry-Standalone
	// header
	Standalone bool `json:"standalone"`
}

// v1Endpoint stores basic information about a V1 registry endpoint.
type v1Endpoint struct {
	client   *http.Client
	URL      *url.URL
	IsSecure bool
}

// newV1Endpoint parses the given address to return a registry endpoint.
// TODO: remove. This is only used by search.
func newV1Endpoint(index *registry.IndexInfo, headers http.Header) (*v1Endpoint, error) {
	tlsConfig, err := newTLSConfig(index.Name, index.Secure)
	if err != nil {
		return nil, err
	}

	endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, headers)
	if err != nil {
		return nil, err
	}

	err = validateEndpoint(endpoint)
	if err != nil {
		return nil, err
	}

	return endpoint, nil
}

func validateEndpoint(endpoint *v1Endpoint) 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 invalidParamf("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.WithError(err).Debugf("error from registry %q marked as insecure - insecurely falling back to HTTP", endpoint)
		endpoint.URL.Scheme = "http"

		var err2 error
		if _, err2 = endpoint.ping(); err2 == nil {
			return nil
		}

		return invalidParamf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
	}

	return nil
}

// trimV1Address trims the version off the address and returns the
// trimmed address or an error if there is a non-V1 version.
func trimV1Address(address string) (string, error) {
	address = strings.TrimSuffix(address, "/")
	chunks := strings.Split(address, "/")
	apiVersionStr := chunks[len(chunks)-1]
	if apiVersionStr == "v1" {
		return strings.Join(chunks[:len(chunks)-1], "/"), nil
	}

	for k, v := range apiVersions {
		if k != APIVersion1 && apiVersionStr == v {
			return "", invalidParamf("unsupported V1 version path %s", apiVersionStr)
		}
	}

	return address, nil
}

func newV1EndpointFromStr(address string, tlsConfig *tls.Config, headers http.Header) (*v1Endpoint, error) {
	if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
		address = "https://" + address
	}

	address, err := trimV1Address(address)
	if err != nil {
		return nil, err
	}

	uri, err := url.Parse(address)
	if err != nil {
		return nil, invalidParam(err)
	}

	// TODO(tiborvass): make sure a ConnectTimeout transport is used
	tr := newTransport(tlsConfig)

	return &v1Endpoint{
		IsSecure: tlsConfig == nil || !tlsConfig.InsecureSkipVerify,
		URL:      uri,
		client:   httpClient(transport.NewTransport(tr, Headers("", headers)...)),
	}, nil
}

// Get the formatted URL for the root of this registry Endpoint
func (e *v1Endpoint) String() string {
	return e.URL.String() + "/v1/"
}

// ping returns a v1PingResult which indicates whether the registry is standalone or not.
func (e *v1Endpoint) ping() (v1PingResult, error) {
	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 v1PingResult{}, nil
	}

	logrus.Debugf("attempting v1 ping for registry endpoint %s", e)
	pingURL := e.String() + "_ping"
	req, err := http.NewRequest(http.MethodGet, pingURL, nil)
	if err != nil {
		return v1PingResult{}, invalidParam(err)
	}

	resp, err := e.client.Do(req)
	if err != nil {
		return v1PingResult{}, invalidParam(err)
	}

	defer resp.Body.Close()

	jsonString, err := io.ReadAll(resp.Body)
	if err != nil {
		return v1PingResult{}, invalidParamWrapf(err, "error while reading response from %s", pingURL)
	}

	// If the header is absent, we assume true for compatibility with earlier
	// versions of the registry. default to true
	info := v1PingResult{
		Standalone: true,
	}
	if err := json.Unmarshal(jsonString, &info); err != nil {
		logrus.WithError(err).Debug("error unmarshaling _ping response")
		// don't stop here. Just assume sane defaults
	}
	if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" {
		info.Version = hdr
	}
	logrus.Debugf("v1PingResult.Version: %q", info.Version)

	standalone := resp.Header.Get("X-Docker-Registry-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("v1PingResult.Standalone: %t", info.Standalone)
	return info, nil
}