Browse code

Move registry to daemon/pkg/registry

Signed-off-by: Derek McGowan <derek@mcg.dev>

Derek McGowan authored on 2025/07/25 04:11:51
Showing 56 changed files
... ...
@@ -5,8 +5,8 @@ import (
5 5
 
6 6
 	"github.com/docker/docker/daemon/config"
7 7
 	"github.com/docker/docker/daemon/pkg/opts"
8
+	"github.com/docker/docker/daemon/pkg/registry"
8 9
 	dopts "github.com/docker/docker/internal/opts"
9
-	"github.com/docker/docker/registry"
10 10
 	"github.com/spf13/pflag"
11 11
 )
12 12
 
... ...
@@ -14,8 +14,8 @@ import (
14 14
 	"dario.cat/mergo"
15 15
 	"github.com/containerd/log"
16 16
 	"github.com/docker/docker/daemon/pkg/opts"
17
+	"github.com/docker/docker/daemon/pkg/registry"
17 18
 	dopts "github.com/docker/docker/internal/opts"
18
-	"github.com/docker/docker/registry"
19 19
 	"github.com/moby/moby/api"
20 20
 	"github.com/moby/moby/api/types/versions"
21 21
 	"github.com/pkg/errors"
... ...
@@ -13,7 +13,7 @@ import (
13 13
 	"dario.cat/mergo"
14 14
 	"github.com/docker/docker/daemon/libnetwork/ipamutils"
15 15
 	"github.com/docker/docker/daemon/pkg/opts"
16
-	"github.com/docker/docker/registry"
16
+	"github.com/docker/docker/daemon/pkg/registry"
17 17
 	"github.com/google/go-cmp/cmp"
18 18
 	"github.com/google/go-cmp/cmp/cmpopts"
19 19
 	"github.com/spf13/pflag"
... ...
@@ -10,9 +10,9 @@ import (
10 10
 	cerrdefs "github.com/containerd/errdefs"
11 11
 	"github.com/containerd/log"
12 12
 	"github.com/distribution/reference"
13
+	"github.com/docker/docker/daemon/pkg/registry"
13 14
 	"github.com/docker/docker/dockerversion"
14 15
 	"github.com/docker/docker/pkg/useragent"
15
-	"github.com/docker/docker/registry"
16 16
 	registrytypes "github.com/moby/moby/api/types/registry"
17 17
 )
18 18
 
... ...
@@ -55,6 +55,7 @@ import (
55 55
 	dlogger "github.com/docker/docker/daemon/logger"
56 56
 	"github.com/docker/docker/daemon/network"
57 57
 	"github.com/docker/docker/daemon/pkg/plugin"
58
+	"github.com/docker/docker/daemon/pkg/registry"
58 59
 	"github.com/docker/docker/daemon/snapshotter"
59 60
 	"github.com/docker/docker/daemon/stats"
60 61
 	volumesservice "github.com/docker/docker/daemon/volume/service"
... ...
@@ -64,7 +65,6 @@ import (
64 64
 	"github.com/docker/docker/pkg/idtools"
65 65
 	"github.com/docker/docker/pkg/plugingetter"
66 66
 	"github.com/docker/docker/pkg/sysinfo"
67
-	"github.com/docker/docker/registry"
68 67
 	"github.com/moby/buildkit/util/grpcerrors"
69 68
 	"github.com/moby/buildkit/util/tracing"
70 69
 	"github.com/moby/locker"
... ...
@@ -16,7 +16,7 @@ import (
16 16
 	"github.com/containerd/containerd/v2/core/remotes/docker"
17 17
 	hostconfig "github.com/containerd/containerd/v2/core/remotes/docker/config"
18 18
 	cerrdefs "github.com/containerd/errdefs"
19
-	"github.com/docker/docker/registry"
19
+	"github.com/docker/docker/daemon/pkg/registry"
20 20
 	"github.com/pkg/errors"
21 21
 )
22 22
 
... ...
@@ -19,13 +19,13 @@ import (
19 19
 	"github.com/docker/docker/daemon/internal/filedescriptors"
20 20
 	"github.com/docker/docker/daemon/internal/metrics"
21 21
 	"github.com/docker/docker/daemon/logger"
22
+	"github.com/docker/docker/daemon/pkg/registry"
22 23
 	"github.com/docker/docker/dockerversion"
23 24
 	"github.com/docker/docker/internal/platform"
24 25
 	"github.com/docker/docker/pkg/meminfo"
25 26
 	"github.com/docker/docker/pkg/parsers/kernel"
26 27
 	"github.com/docker/docker/pkg/parsers/operatingsystem"
27 28
 	"github.com/docker/docker/pkg/sysinfo"
28
-	"github.com/docker/docker/registry"
29 29
 	"github.com/moby/moby/api/types"
30 30
 	"github.com/moby/moby/api/types/system"
31 31
 	"github.com/opencontainers/selinux/go-selinux"
... ...
@@ -14,8 +14,8 @@ import (
14 14
 	"github.com/docker/docker/daemon/internal/image"
15 15
 	"github.com/docker/docker/daemon/internal/layer"
16 16
 	refstore "github.com/docker/docker/daemon/internal/refstore"
17
+	registrypkg "github.com/docker/docker/daemon/pkg/registry"
17 18
 	"github.com/docker/docker/pkg/progress"
18
-	registrypkg "github.com/docker/docker/registry"
19 19
 	"github.com/moby/moby/api/types/events"
20 20
 	"github.com/moby/moby/api/types/registry"
21 21
 	"github.com/opencontainers/go-digest"
... ...
@@ -15,7 +15,7 @@ import (
15 15
 	"github.com/docker/distribution"
16 16
 	"github.com/docker/distribution/manifest/manifestlist"
17 17
 	"github.com/docker/distribution/manifest/schema2"
18
-	"github.com/docker/docker/registry"
18
+	"github.com/docker/docker/daemon/pkg/registry"
19 19
 	"github.com/opencontainers/go-digest"
20 20
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
21 21
 	"github.com/pkg/errors"
... ...
@@ -7,7 +7,7 @@ import (
7 7
 	"github.com/containerd/log"
8 8
 	"github.com/distribution/reference"
9 9
 	refstore "github.com/docker/docker/daemon/internal/refstore"
10
-	"github.com/docker/docker/registry"
10
+	"github.com/docker/docker/daemon/pkg/registry"
11 11
 	"github.com/moby/moby/api/types/events"
12 12
 	"github.com/opencontainers/go-digest"
13 13
 	"github.com/pkg/errors"
... ...
@@ -22,10 +22,10 @@ import (
22 22
 	"github.com/docker/docker/daemon/internal/image"
23 23
 	"github.com/docker/docker/daemon/internal/layer"
24 24
 	refstore "github.com/docker/docker/daemon/internal/refstore"
25
+	"github.com/docker/docker/daemon/pkg/registry"
25 26
 	"github.com/docker/docker/pkg/ioutils"
26 27
 	"github.com/docker/docker/pkg/progress"
27 28
 	"github.com/docker/docker/pkg/stringid"
28
-	"github.com/docker/docker/registry"
29 29
 	"github.com/opencontainers/go-digest"
30 30
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
31 31
 	"github.com/pkg/errors"
... ...
@@ -12,7 +12,7 @@ import (
12 12
 
13 13
 	"github.com/distribution/reference"
14 14
 	"github.com/docker/docker/daemon/internal/image"
15
-	"github.com/docker/docker/registry"
15
+	"github.com/docker/docker/daemon/pkg/registry"
16 16
 	registrytypes "github.com/moby/moby/api/types/registry"
17 17
 	"github.com/opencontainers/go-digest"
18 18
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
... ...
@@ -17,10 +17,10 @@ import (
17 17
 	"github.com/docker/docker/daemon/internal/distribution/metadata"
18 18
 	"github.com/docker/docker/daemon/internal/distribution/xfer"
19 19
 	"github.com/docker/docker/daemon/internal/layer"
20
+	"github.com/docker/docker/daemon/pkg/registry"
20 21
 	"github.com/docker/docker/pkg/ioutils"
21 22
 	"github.com/docker/docker/pkg/progress"
22 23
 	"github.com/docker/docker/pkg/stringid"
23
-	"github.com/docker/docker/registry"
24 24
 	apitypes "github.com/moby/moby/api/types"
25 25
 	"github.com/opencontainers/go-digest"
26 26
 	"github.com/pkg/errors"
... ...
@@ -13,8 +13,8 @@ import (
13 13
 	"github.com/docker/docker/daemon/internal/distribution/metadata"
14 14
 	"github.com/docker/docker/daemon/internal/layer"
15 15
 	refstore "github.com/docker/docker/daemon/internal/refstore"
16
+	registrypkg "github.com/docker/docker/daemon/pkg/registry"
16 17
 	"github.com/docker/docker/pkg/progress"
17
-	registrypkg "github.com/docker/docker/registry"
18 18
 	"github.com/moby/moby/api/types/registry"
19 19
 	"github.com/opencontainers/go-digest"
20 20
 	"gotest.tools/v3/assert"
... ...
@@ -12,8 +12,8 @@ import (
12 12
 	"github.com/docker/distribution/manifest/schema2"
13 13
 	"github.com/docker/distribution/registry/client"
14 14
 	"github.com/docker/distribution/registry/client/auth"
15
+	"github.com/docker/docker/daemon/pkg/registry"
15 16
 	"github.com/docker/docker/dockerversion"
16
-	"github.com/docker/docker/registry"
17 17
 	registrytypes "github.com/moby/moby/api/types/registry"
18 18
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
19 19
 )
... ...
@@ -9,7 +9,7 @@ import (
9 9
 	"testing"
10 10
 
11 11
 	"github.com/distribution/reference"
12
-	registrypkg "github.com/docker/docker/registry"
12
+	registrypkg "github.com/docker/docker/daemon/pkg/registry"
13 13
 	"github.com/moby/moby/api/types/registry"
14 14
 	"gotest.tools/v3/assert"
15 15
 )
... ...
@@ -16,10 +16,10 @@ import (
16 16
 	"github.com/containerd/containerd/v2/plugins/content/local"
17 17
 	"github.com/containerd/log"
18 18
 	v2 "github.com/docker/docker/daemon/pkg/plugin/v2"
19
+	"github.com/docker/docker/daemon/pkg/registry"
19 20
 	"github.com/docker/docker/internal/containerfs"
20 21
 	"github.com/docker/docker/internal/lazyregexp"
21 22
 	"github.com/docker/docker/pkg/authorization"
22
-	"github.com/docker/docker/registry"
23 23
 	"github.com/moby/moby/api/types"
24 24
 	"github.com/moby/moby/api/types/events"
25 25
 	"github.com/moby/pubsub"
26 26
new file mode 100644
... ...
@@ -0,0 +1,205 @@
0
+package registry
1
+
2
+import (
3
+	"context"
4
+	"net/http"
5
+	"net/url"
6
+	"strings"
7
+	"time"
8
+
9
+	"github.com/containerd/log"
10
+	"github.com/docker/distribution/registry/client/auth"
11
+	"github.com/docker/distribution/registry/client/auth/challenge"
12
+	"github.com/docker/distribution/registry/client/transport"
13
+	"github.com/moby/moby/api/types/registry"
14
+	"github.com/pkg/errors"
15
+)
16
+
17
+// AuthClientID is used the ClientID used for the token server
18
+const AuthClientID = "docker"
19
+
20
+type loginCredentialStore struct {
21
+	authConfig *registry.AuthConfig
22
+}
23
+
24
+func (lcs loginCredentialStore) Basic(*url.URL) (string, string) {
25
+	return lcs.authConfig.Username, lcs.authConfig.Password
26
+}
27
+
28
+func (lcs loginCredentialStore) RefreshToken(*url.URL, string) string {
29
+	return lcs.authConfig.IdentityToken
30
+}
31
+
32
+func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token string) {
33
+	lcs.authConfig.IdentityToken = token
34
+}
35
+
36
+type staticCredentialStore struct {
37
+	auth *registry.AuthConfig
38
+}
39
+
40
+// NewStaticCredentialStore returns a credential store
41
+// which always returns the same credential values.
42
+func NewStaticCredentialStore(ac *registry.AuthConfig) auth.CredentialStore {
43
+	return staticCredentialStore{
44
+		auth: ac,
45
+	}
46
+}
47
+
48
+func (scs staticCredentialStore) Basic(*url.URL) (string, string) {
49
+	if scs.auth == nil {
50
+		return "", ""
51
+	}
52
+	return scs.auth.Username, scs.auth.Password
53
+}
54
+
55
+func (scs staticCredentialStore) RefreshToken(*url.URL, string) string {
56
+	if scs.auth == nil {
57
+		return ""
58
+	}
59
+	return scs.auth.IdentityToken
60
+}
61
+
62
+func (staticCredentialStore) SetRefreshToken(*url.URL, string, string) {
63
+}
64
+
65
+// loginV2 tries to login to the v2 registry server. The given registry
66
+// endpoint will be pinged to get authorization challenges. These challenges
67
+// will be used to authenticate against the registry to validate credentials.
68
+func loginV2(ctx context.Context, authConfig *registry.AuthConfig, endpoint APIEndpoint, userAgent string) (token string, _ error) {
69
+	endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
70
+	log.G(ctx).WithField("endpoint", endpointStr).Debug("attempting v2 login to registry endpoint")
71
+
72
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpointStr, http.NoBody)
73
+	if err != nil {
74
+		return "", err
75
+	}
76
+
77
+	var (
78
+		modifiers            = Headers(userAgent, nil)
79
+		authTrans            = transport.NewTransport(newTransport(endpoint.TLSConfig), modifiers...)
80
+		credentialAuthConfig = *authConfig
81
+		creds                = loginCredentialStore{authConfig: &credentialAuthConfig}
82
+	)
83
+
84
+	loginClient, err := v2AuthHTTPClient(endpoint.URL, authTrans, modifiers, creds, nil)
85
+	if err != nil {
86
+		return "", err
87
+	}
88
+
89
+	resp, err := loginClient.Do(req)
90
+	if err != nil {
91
+		err = translateV2AuthError(err)
92
+		return "", err
93
+	}
94
+	defer resp.Body.Close()
95
+
96
+	if resp.StatusCode != http.StatusOK {
97
+		// TODO(dmcgowan): Attempt to further interpret result, status code and error code string
98
+		return "", errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
99
+	}
100
+
101
+	return credentialAuthConfig.IdentityToken, nil
102
+}
103
+
104
+func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, error) {
105
+	challengeManager, err := PingV2Registry(endpoint, authTransport)
106
+	if err != nil {
107
+		return nil, err
108
+	}
109
+
110
+	authHandlers := []auth.AuthenticationHandler{
111
+		auth.NewTokenHandlerWithOptions(auth.TokenHandlerOptions{
112
+			Transport:     authTransport,
113
+			Credentials:   creds,
114
+			OfflineAccess: true,
115
+			ClientID:      AuthClientID,
116
+			Scopes:        scopes,
117
+		}),
118
+		auth.NewBasicHandler(creds),
119
+	}
120
+
121
+	modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, authHandlers...))
122
+
123
+	return &http.Client{
124
+		Transport: transport.NewTransport(authTransport, modifiers...),
125
+		Timeout:   15 * time.Second,
126
+	}, nil
127
+}
128
+
129
+// ConvertToHostname normalizes a registry URL which has http|https prepended
130
+// to just its hostname. It is used to match credentials, which may be either
131
+// stored as hostname or as hostname including scheme (in legacy configuration
132
+// files).
133
+func ConvertToHostname(maybeURL string) string {
134
+	stripped := maybeURL
135
+	if scheme, remainder, ok := strings.Cut(stripped, "://"); ok {
136
+		switch scheme {
137
+		case "http", "https":
138
+			stripped = remainder
139
+		default:
140
+			// unknown, or no scheme; doing nothing for now, as we never did.
141
+		}
142
+	}
143
+	stripped, _, _ = strings.Cut(stripped, "/")
144
+	return stripped
145
+}
146
+
147
+// ResolveAuthConfig matches an auth configuration to a server address or a URL
148
+func ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, index *registry.IndexInfo) registry.AuthConfig {
149
+	configKey := GetAuthConfigKey(index)
150
+	// First try the happy case
151
+	if c, found := authConfigs[configKey]; found || index.Official {
152
+		return c
153
+	}
154
+
155
+	// Maybe they have a legacy config file, we will iterate the keys converting
156
+	// them to the new format and testing
157
+	for registryURL, ac := range authConfigs {
158
+		if configKey == ConvertToHostname(registryURL) {
159
+			return ac
160
+		}
161
+	}
162
+
163
+	// When all else fails, return an empty auth config
164
+	return registry.AuthConfig{}
165
+}
166
+
167
+// PingResponseError is used when the response from a ping
168
+// was received but invalid.
169
+type PingResponseError struct {
170
+	Err error
171
+}
172
+
173
+func (err PingResponseError) Error() string {
174
+	return err.Err.Error()
175
+}
176
+
177
+// PingV2Registry attempts to ping a v2 registry and on success return a
178
+// challenge manager for the supported authentication types.
179
+// If a response is received but cannot be interpreted, a PingResponseError will be returned.
180
+func PingV2Registry(endpoint *url.URL, authTransport http.RoundTripper) (challenge.Manager, error) {
181
+	pingClient := &http.Client{
182
+		Transport: authTransport,
183
+		Timeout:   15 * time.Second,
184
+	}
185
+	endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/"
186
+	req, err := http.NewRequest(http.MethodGet, endpointStr, http.NoBody)
187
+	if err != nil {
188
+		return nil, err
189
+	}
190
+	resp, err := pingClient.Do(req)
191
+	if err != nil {
192
+		return nil, err
193
+	}
194
+	defer resp.Body.Close()
195
+
196
+	challengeManager := challenge.NewSimpleManager()
197
+	if err := challengeManager.AddResponse(resp); err != nil {
198
+		return nil, PingResponseError{
199
+			Err: err,
200
+		}
201
+	}
202
+
203
+	return challengeManager, nil
204
+}
0 205
new file mode 100644
... ...
@@ -0,0 +1,106 @@
0
+package registry
1
+
2
+import (
3
+	"testing"
4
+
5
+	"github.com/moby/moby/api/types/registry"
6
+	"gotest.tools/v3/assert"
7
+)
8
+
9
+func buildAuthConfigs() map[string]registry.AuthConfig {
10
+	authConfigs := map[string]registry.AuthConfig{}
11
+
12
+	for _, reg := range []string{"testIndex", IndexServer} {
13
+		authConfigs[reg] = registry.AuthConfig{
14
+			Username: "docker-user",
15
+			Password: "docker-pass",
16
+		}
17
+	}
18
+
19
+	return authConfigs
20
+}
21
+
22
+func TestResolveAuthConfigIndexServer(t *testing.T) {
23
+	authConfigs := buildAuthConfigs()
24
+	indexConfig := authConfigs[IndexServer]
25
+
26
+	officialIndex := &registry.IndexInfo{
27
+		Official: true,
28
+	}
29
+	privateIndex := &registry.IndexInfo{
30
+		Official: false,
31
+	}
32
+
33
+	resolved := ResolveAuthConfig(authConfigs, officialIndex)
34
+	assert.Equal(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServer")
35
+
36
+	resolved = ResolveAuthConfig(authConfigs, privateIndex)
37
+	assert.Check(t, resolved != indexConfig, "Expected ResolveAuthConfig to not return IndexServer")
38
+}
39
+
40
+func TestResolveAuthConfigFullURL(t *testing.T) {
41
+	authConfigs := buildAuthConfigs()
42
+
43
+	registryAuth := registry.AuthConfig{
44
+		Username: "foo-user",
45
+		Password: "foo-pass",
46
+	}
47
+	localAuth := registry.AuthConfig{
48
+		Username: "bar-user",
49
+		Password: "bar-pass",
50
+	}
51
+	officialAuth := registry.AuthConfig{
52
+		Username: "baz-user",
53
+		Password: "baz-pass",
54
+	}
55
+	authConfigs[IndexServer] = officialAuth
56
+
57
+	expectedAuths := map[string]registry.AuthConfig{
58
+		"registry.example.com": registryAuth,
59
+		"localhost:8000":       localAuth,
60
+		"example.com":          localAuth,
61
+	}
62
+
63
+	validRegistries := map[string][]string{
64
+		"registry.example.com": {
65
+			"https://registry.example.com/v1/",
66
+			"http://registry.example.com/v1/",
67
+			"registry.example.com",
68
+			"registry.example.com/v1/",
69
+		},
70
+		"localhost:8000": {
71
+			"https://localhost:8000/v1/",
72
+			"http://localhost:8000/v1/",
73
+			"localhost:8000",
74
+			"localhost:8000/v1/",
75
+		},
76
+		"example.com": {
77
+			"https://example.com/v1/",
78
+			"http://example.com/v1/",
79
+			"example.com",
80
+			"example.com/v1/",
81
+		},
82
+	}
83
+
84
+	for configKey, registries := range validRegistries {
85
+		configured, ok := expectedAuths[configKey]
86
+		if !ok {
87
+			t.Fail()
88
+		}
89
+		index := &registry.IndexInfo{
90
+			Name: configKey,
91
+		}
92
+		for _, reg := range registries {
93
+			authConfigs[reg] = configured
94
+			resolved := ResolveAuthConfig(authConfigs, index)
95
+			if resolved.Username != configured.Username || resolved.Password != configured.Password {
96
+				t.Errorf("%s -> %v != %v\n", reg, resolved, configured)
97
+			}
98
+			delete(authConfigs, reg)
99
+			resolved = ResolveAuthConfig(authConfigs, index)
100
+			if resolved.Username == configured.Username || resolved.Password == configured.Password {
101
+				t.Errorf("%s -> %v == %v\n", reg, resolved, configured)
102
+			}
103
+		}
104
+	}
105
+}
0 106
new file mode 100644
... ...
@@ -0,0 +1,449 @@
0
+// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
1
+//go:build go1.23
2
+
3
+package registry
4
+
5
+import (
6
+	"context"
7
+	"net"
8
+	"net/url"
9
+	"os"
10
+	"path/filepath"
11
+	"regexp"
12
+	"runtime"
13
+	"strconv"
14
+	"strings"
15
+	"sync"
16
+
17
+	"github.com/containerd/log"
18
+	"github.com/distribution/reference"
19
+	"github.com/moby/moby/api/types/registry"
20
+)
21
+
22
+// ServiceOptions holds command line options.
23
+type ServiceOptions struct {
24
+	Mirrors            []string `json:"registry-mirrors,omitempty"`
25
+	InsecureRegistries []string `json:"insecure-registries,omitempty"`
26
+}
27
+
28
+// serviceConfig holds daemon configuration for the registry service.
29
+type serviceConfig registry.ServiceConfig
30
+
31
+// TODO(thaJeztah) both the "index.docker.io" and "registry-1.docker.io" domains
32
+// are here for historic reasons and backward-compatibility. These domains
33
+// are still supported by Docker Hub (and will continue to be supported), but
34
+// there are new domains already in use, and plans to consolidate all legacy
35
+// domains to new "canonical" domains. Once those domains are decided on, we
36
+// should update these consts (but making sure to preserve compatibility with
37
+// existing installs, clients, and user configuration).
38
+const (
39
+	// DefaultNamespace is the default namespace
40
+	DefaultNamespace = "docker.io"
41
+	// DefaultRegistryHost is the hostname for the default (Docker Hub) registry
42
+	// used for pushing and pulling images. This hostname is hard-coded to handle
43
+	// the conversion from image references without registry name (e.g. "ubuntu",
44
+	// or "ubuntu:latest"), as well as references using the "docker.io" domain
45
+	// name, which is used as canonical reference for images on Docker Hub, but
46
+	// does not match the domain-name of Docker Hub's registry.
47
+	DefaultRegistryHost = "registry-1.docker.io"
48
+	// IndexHostname is the index hostname, used for authentication and image search.
49
+	IndexHostname = "index.docker.io"
50
+	// IndexServer is used for user auth and image search
51
+	IndexServer = "https://" + IndexHostname + "/v1/"
52
+	// IndexName is the name of the index
53
+	IndexName = "docker.io"
54
+)
55
+
56
+var (
57
+	// DefaultV2Registry is the URI of the default (Docker Hub) registry.
58
+	DefaultV2Registry = &url.URL{
59
+		Scheme: "https",
60
+		Host:   DefaultRegistryHost,
61
+	}
62
+
63
+	validHostPortRegex = sync.OnceValue(func() *regexp.Regexp {
64
+		return regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`)
65
+	})
66
+)
67
+
68
+// runningWithRootlessKit is a fork of [rootless.RunningWithRootlessKit],
69
+// but inlining it to prevent adding that as a dependency for docker/cli.
70
+//
71
+// [rootless.RunningWithRootlessKit]: https://github.com/moby/moby/blob/b4bdf12daec84caaf809a639f923f7370d4926ad/pkg/rootless/rootless.go#L5-L8
72
+func runningWithRootlessKit() bool {
73
+	return runtime.GOOS == "linux" && os.Getenv("ROOTLESSKIT_STATE_DIR") != ""
74
+}
75
+
76
+// CertsDir is the directory where certificates are stored.
77
+//
78
+// - Linux: "/etc/docker/certs.d/"
79
+// - Linux (with rootlessKit): $XDG_CONFIG_HOME/docker/certs.d/" or "$HOME/.config/docker/certs.d/"
80
+// - Windows: "%PROGRAMDATA%/docker/certs.d/"
81
+//
82
+// TODO(thaJeztah): certsDir but stored in our config, and passed when needed. For the CLI, we should also default to same path as rootless.
83
+func CertsDir() string {
84
+	certsDir := "/etc/docker/certs.d"
85
+	if runningWithRootlessKit() {
86
+		if configHome, _ := os.UserConfigDir(); configHome != "" {
87
+			certsDir = filepath.Join(configHome, "docker", "certs.d")
88
+		}
89
+	} else if runtime.GOOS == "windows" {
90
+		certsDir = filepath.Join(os.Getenv("programdata"), "docker", "certs.d")
91
+	}
92
+	return certsDir
93
+}
94
+
95
+// newServiceConfig returns a new instance of ServiceConfig
96
+func newServiceConfig(options ServiceOptions) (*serviceConfig, error) {
97
+	config := &serviceConfig{}
98
+	if err := config.loadMirrors(options.Mirrors); err != nil {
99
+		return nil, err
100
+	}
101
+	if err := config.loadInsecureRegistries(options.InsecureRegistries); err != nil {
102
+		return nil, err
103
+	}
104
+
105
+	return config, nil
106
+}
107
+
108
+// copy constructs a new ServiceConfig with a copy of the configuration in config.
109
+func (config *serviceConfig) copy() *registry.ServiceConfig {
110
+	ic := make(map[string]*registry.IndexInfo)
111
+	for key, value := range config.IndexConfigs {
112
+		ic[key] = value
113
+	}
114
+	return &registry.ServiceConfig{
115
+		InsecureRegistryCIDRs: append([]*registry.NetIPNet(nil), config.InsecureRegistryCIDRs...),
116
+		IndexConfigs:          ic,
117
+		Mirrors:               append([]string(nil), config.Mirrors...),
118
+	}
119
+}
120
+
121
+// loadMirrors loads mirrors to config, after removing duplicates.
122
+// Returns an error if mirrors contains an invalid mirror.
123
+func (config *serviceConfig) loadMirrors(mirrors []string) error {
124
+	mMap := map[string]struct{}{}
125
+	unique := []string{}
126
+
127
+	for _, mirror := range mirrors {
128
+		m, err := ValidateMirror(mirror)
129
+		if err != nil {
130
+			return err
131
+		}
132
+		if _, exist := mMap[m]; !exist {
133
+			mMap[m] = struct{}{}
134
+			unique = append(unique, m)
135
+		}
136
+	}
137
+
138
+	config.Mirrors = unique
139
+
140
+	// Configure public registry since mirrors may have changed.
141
+	config.IndexConfigs = map[string]*registry.IndexInfo{
142
+		IndexName: {
143
+			Name:     IndexName,
144
+			Mirrors:  unique,
145
+			Secure:   true,
146
+			Official: true,
147
+		},
148
+	}
149
+
150
+	return nil
151
+}
152
+
153
+// loadInsecureRegistries loads insecure registries to config
154
+func (config *serviceConfig) loadInsecureRegistries(registries []string) error {
155
+	// Localhost is by default considered as an insecure registry. This is a
156
+	// stop-gap for people who are running a private registry on localhost.
157
+	registries = append(registries, "::1/128", "127.0.0.0/8")
158
+
159
+	var (
160
+		insecureRegistryCIDRs = make([]*registry.NetIPNet, 0)
161
+		indexConfigs          = make(map[string]*registry.IndexInfo)
162
+	)
163
+
164
+skip:
165
+	for _, r := range registries {
166
+		// validate insecure registry
167
+		if _, err := ValidateIndexName(r); err != nil {
168
+			return err
169
+		}
170
+		if scheme, host, ok := strings.Cut(r, "://"); ok {
171
+			switch strings.ToLower(scheme) {
172
+			case "http", "https":
173
+				log.G(context.TODO()).Warnf("insecure registry %[1]s should not contain '%[2]s' and '%[2]ss' has been removed from the insecure registry config", r, scheme)
174
+				r = host
175
+			default:
176
+				// unsupported scheme
177
+				return invalidParamf("insecure registry %s should not contain '://'", r)
178
+			}
179
+		}
180
+		// Check if CIDR was passed to --insecure-registry
181
+		_, ipnet, err := net.ParseCIDR(r)
182
+		if err == nil {
183
+			// Valid CIDR. If ipnet is already in config.InsecureRegistryCIDRs, skip.
184
+			data := (*registry.NetIPNet)(ipnet)
185
+			for _, value := range insecureRegistryCIDRs {
186
+				if value.IP.String() == data.IP.String() && value.Mask.String() == data.Mask.String() {
187
+					continue skip
188
+				}
189
+			}
190
+			// ipnet is not found, add it in config.InsecureRegistryCIDRs
191
+			insecureRegistryCIDRs = append(insecureRegistryCIDRs, data)
192
+		} else {
193
+			if err := validateHostPort(r); err != nil {
194
+				return invalidParamWrapf(err, "insecure registry %s is not valid", r)
195
+			}
196
+			// Assume `host:port` if not CIDR.
197
+			indexConfigs[r] = &registry.IndexInfo{
198
+				Name:     r,
199
+				Mirrors:  []string{},
200
+				Secure:   false,
201
+				Official: false,
202
+			}
203
+		}
204
+	}
205
+
206
+	// Configure public registry.
207
+	indexConfigs[IndexName] = &registry.IndexInfo{
208
+		Name:     IndexName,
209
+		Mirrors:  config.Mirrors,
210
+		Secure:   true,
211
+		Official: true,
212
+	}
213
+	config.InsecureRegistryCIDRs = insecureRegistryCIDRs
214
+	config.IndexConfigs = indexConfigs
215
+
216
+	return nil
217
+}
218
+
219
+// isSecureIndex returns false if the provided indexName is part of the list of insecure registries
220
+// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs.
221
+//
222
+// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet.
223
+// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered
224
+// insecure.
225
+//
226
+// indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name
227
+// or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained
228
+// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element
229
+// of insecureRegistries.
230
+func (config *serviceConfig) isSecureIndex(indexName string) bool {
231
+	// Check for configured index, first.  This is needed in case isSecureIndex
232
+	// is called from anything besides newIndexInfo, in order to honor per-index configurations.
233
+	if index, ok := config.IndexConfigs[indexName]; ok {
234
+		return index.Secure
235
+	}
236
+
237
+	return !isCIDRMatch(config.InsecureRegistryCIDRs, indexName)
238
+}
239
+
240
+// for mocking in unit tests.
241
+var lookupIP = net.LookupIP
242
+
243
+// isCIDRMatch returns true if urlHost matches an element of cidrs. urlHost is a URL.Host ("host:port" or "host")
244
+// where the `host` part can be either a domain name or an IP address. If it is a domain name, then it will be
245
+// resolved to IP addresses for matching. If resolution fails, false is returned.
246
+func isCIDRMatch(cidrs []*registry.NetIPNet, urlHost string) bool {
247
+	if len(cidrs) == 0 {
248
+		return false
249
+	}
250
+
251
+	host, _, err := net.SplitHostPort(urlHost)
252
+	if err != nil {
253
+		// Assume urlHost is a host without port and go on.
254
+		host = urlHost
255
+	}
256
+
257
+	var addresses []net.IP
258
+	if ip := net.ParseIP(host); ip != nil {
259
+		// Host is an IP-address.
260
+		addresses = append(addresses, ip)
261
+	} else {
262
+		// Try to resolve the host's IP-address.
263
+		addresses, err = lookupIP(host)
264
+		if err != nil {
265
+			// We failed to resolve the host; assume there's no match.
266
+			return false
267
+		}
268
+	}
269
+
270
+	for _, addr := range addresses {
271
+		for _, ipnet := range cidrs {
272
+			// check if the addr falls in the subnet
273
+			if (*net.IPNet)(ipnet).Contains(addr) {
274
+				return true
275
+			}
276
+		}
277
+	}
278
+
279
+	return false
280
+}
281
+
282
+// ValidateMirror validates and normalizes an HTTP(S) registry mirror. It
283
+// returns an error if the given mirrorURL is invalid, or the normalized
284
+// format for the URL otherwise.
285
+//
286
+// It is used by the daemon to validate the daemon configuration.
287
+func ValidateMirror(mirrorURL string) (string, error) {
288
+	// Fast path for missing scheme, as url.Parse splits by ":", which can
289
+	// cause the hostname to be considered the "scheme" when using "hostname:port".
290
+	if scheme, _, ok := strings.Cut(mirrorURL, "://"); !ok || scheme == "" {
291
+		return "", invalidParamf("invalid mirror: no scheme specified for %q: must use either 'https://' or 'http://'", mirrorURL)
292
+	}
293
+	uri, err := url.Parse(mirrorURL)
294
+	if err != nil {
295
+		return "", invalidParamWrapf(err, "invalid mirror: %q is not a valid URI", mirrorURL)
296
+	}
297
+	if uri.Scheme != "http" && uri.Scheme != "https" {
298
+		return "", invalidParamf("invalid mirror: unsupported scheme %q in %q: must use either 'https://' or 'http://'", uri.Scheme, uri)
299
+	}
300
+	if uri.RawQuery != "" || uri.Fragment != "" {
301
+		return "", invalidParamf("invalid mirror: query or fragment at end of the URI %q", uri)
302
+	}
303
+	if uri.User != nil {
304
+		// strip password from output
305
+		uri.User = url.UserPassword(uri.User.Username(), "xxxxx")
306
+		return "", invalidParamf("invalid mirror: username/password not allowed in URI %q", uri)
307
+	}
308
+	return strings.TrimSuffix(mirrorURL, "/") + "/", nil
309
+}
310
+
311
+// ValidateIndexName validates an index name. It is used by the daemon to
312
+// validate the daemon configuration.
313
+func ValidateIndexName(val string) (string, error) {
314
+	val = normalizeIndexName(val)
315
+	if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") {
316
+		return "", invalidParamf("invalid index name (%s). Cannot begin or end with a hyphen", val)
317
+	}
318
+	return val, nil
319
+}
320
+
321
+func normalizeIndexName(val string) string {
322
+	// TODO(thaJeztah): consider normalizing other known options, such as "(https://)registry-1.docker.io", "https://index.docker.io/v1/".
323
+	// TODO: upstream this to check to reference package
324
+	if val == "index.docker.io" {
325
+		return "docker.io"
326
+	}
327
+	return val
328
+}
329
+
330
+func hasScheme(reposName string) bool {
331
+	return strings.Contains(reposName, "://")
332
+}
333
+
334
+func validateHostPort(s string) error {
335
+	// Split host and port, and in case s can not be split, assume host only
336
+	host, port, err := net.SplitHostPort(s)
337
+	if err != nil {
338
+		host = s
339
+		port = ""
340
+	}
341
+	// If match against the `host:port` pattern fails,
342
+	// it might be `IPv6:port`, which will be captured by net.ParseIP(host)
343
+	if !validHostPortRegex().MatchString(s) && net.ParseIP(host) == nil {
344
+		return invalidParamf("invalid host %q", host)
345
+	}
346
+	if port != "" {
347
+		v, err := strconv.Atoi(port)
348
+		if err != nil {
349
+			return err
350
+		}
351
+		if v < 0 || v > 65535 {
352
+			return invalidParamf("invalid port %q", port)
353
+		}
354
+	}
355
+	return nil
356
+}
357
+
358
+// newIndexInfo returns IndexInfo configuration from indexName
359
+func newIndexInfo(config *serviceConfig, indexName string) *registry.IndexInfo {
360
+	indexName = normalizeIndexName(indexName)
361
+
362
+	// Return any configured index info, first.
363
+	if index, ok := config.IndexConfigs[indexName]; ok {
364
+		return index
365
+	}
366
+
367
+	// Construct a non-configured index info.
368
+	return &registry.IndexInfo{
369
+		Name:    indexName,
370
+		Mirrors: []string{},
371
+		Secure:  config.isSecureIndex(indexName),
372
+	}
373
+}
374
+
375
+// GetAuthConfigKey special-cases using the full index address of the official
376
+// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
377
+func GetAuthConfigKey(index *registry.IndexInfo) string {
378
+	if index.Official {
379
+		return IndexServer
380
+	}
381
+	return index.Name
382
+}
383
+
384
+// ParseRepositoryInfo performs the breakdown of a repository name into a
385
+// [RepositoryInfo], but lacks registry configuration.
386
+//
387
+// It is used by the Docker cli to interact with registry-related endpoints.
388
+func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
389
+	indexName := normalizeIndexName(reference.Domain(reposName))
390
+	if indexName == IndexName {
391
+		return &RepositoryInfo{
392
+			Name: reference.TrimNamed(reposName),
393
+			Index: &registry.IndexInfo{
394
+				Name:     IndexName,
395
+				Mirrors:  []string{},
396
+				Secure:   true,
397
+				Official: true,
398
+			},
399
+		}, nil
400
+	}
401
+
402
+	return &RepositoryInfo{
403
+		Name: reference.TrimNamed(reposName),
404
+		Index: &registry.IndexInfo{
405
+			Name:    indexName,
406
+			Mirrors: []string{},
407
+			Secure:  !isInsecure(indexName),
408
+		},
409
+	}, nil
410
+}
411
+
412
+// isInsecure is used to detect whether a registry domain or IP-address is allowed
413
+// to use an insecure (non-TLS, or self-signed cert) connection according to the
414
+// defaults, which allows for insecure connections with registries running on a
415
+// loopback address ("localhost", "::1/128", "127.0.0.0/8").
416
+//
417
+// It is used in situations where we don't have access to the daemon's configuration,
418
+// for example, when used from the client / CLI.
419
+func isInsecure(hostNameOrIP string) bool {
420
+	// Attempt to strip port if present; this also strips brackets for
421
+	// IPv6 addresses with a port (e.g. "[::1]:5000").
422
+	//
423
+	// This is best-effort; we'll continue using the address as-is if it fails.
424
+	if host, _, err := net.SplitHostPort(hostNameOrIP); err == nil {
425
+		hostNameOrIP = host
426
+	}
427
+	if hostNameOrIP == "127.0.0.1" || hostNameOrIP == "::1" || strings.EqualFold(hostNameOrIP, "localhost") {
428
+		// Fast path; no need to resolve these, assuming nobody overrides
429
+		// "localhost" for anything else than a loopback address (sorry, not sorry).
430
+		return true
431
+	}
432
+
433
+	var addresses []net.IP
434
+	if ip := net.ParseIP(hostNameOrIP); ip != nil {
435
+		addresses = append(addresses, ip)
436
+	} else {
437
+		// Try to resolve the host's IP-addresses.
438
+		addrs, _ := lookupIP(hostNameOrIP)
439
+		addresses = append(addresses, addrs...)
440
+	}
441
+
442
+	for _, addr := range addresses {
443
+		if addr.IsLoopback() {
444
+			return true
445
+		}
446
+	}
447
+	return false
448
+}
0 449
new file mode 100644
... ...
@@ -0,0 +1,340 @@
0
+package registry
1
+
2
+import (
3
+	"testing"
4
+
5
+	cerrdefs "github.com/containerd/errdefs"
6
+	"gotest.tools/v3/assert"
7
+	is "gotest.tools/v3/assert/cmp"
8
+)
9
+
10
+func TestValidateMirror(t *testing.T) {
11
+	tests := []struct {
12
+		input       string
13
+		output      string
14
+		expectedErr string
15
+	}{
16
+		// Valid cases
17
+		{
18
+			input:  "http://mirror-1.example.com",
19
+			output: "http://mirror-1.example.com/",
20
+		},
21
+		{
22
+			input:  "http://mirror-1.example.com/",
23
+			output: "http://mirror-1.example.com/",
24
+		},
25
+		{
26
+			input:  "https://mirror-1.example.com",
27
+			output: "https://mirror-1.example.com/",
28
+		},
29
+		{
30
+			input:  "https://mirror-1.example.com/",
31
+			output: "https://mirror-1.example.com/",
32
+		},
33
+		{
34
+			input:  "http://localhost",
35
+			output: "http://localhost/",
36
+		},
37
+		{
38
+			input:  "https://localhost",
39
+			output: "https://localhost/",
40
+		},
41
+		{
42
+			input:  "http://localhost:5000",
43
+			output: "http://localhost:5000/",
44
+		},
45
+		{
46
+			input:  "https://localhost:5000",
47
+			output: "https://localhost:5000/",
48
+		},
49
+		{
50
+			input:  "http://127.0.0.1",
51
+			output: "http://127.0.0.1/",
52
+		},
53
+		{
54
+			input:  "https://127.0.0.1",
55
+			output: "https://127.0.0.1/",
56
+		},
57
+		{
58
+			input:  "http://127.0.0.1:5000",
59
+			output: "http://127.0.0.1:5000/",
60
+		},
61
+		{
62
+			input:  "https://127.0.0.1:5000",
63
+			output: "https://127.0.0.1:5000/",
64
+		},
65
+		{
66
+			input:  "http://mirror-1.example.com/v1/",
67
+			output: "http://mirror-1.example.com/v1/",
68
+		},
69
+		{
70
+			input:  "https://mirror-1.example.com/v1/",
71
+			output: "https://mirror-1.example.com/v1/",
72
+		},
73
+
74
+		// Invalid cases
75
+		{
76
+			input:       "!invalid!://%as%",
77
+			expectedErr: `invalid mirror: "!invalid!://%as%" is not a valid URI: parse "!invalid!://%as%": first path segment in URL cannot contain colon`,
78
+		},
79
+		{
80
+			input:       "mirror-1.example.com",
81
+			expectedErr: `invalid mirror: no scheme specified for "mirror-1.example.com": must use either 'https://' or 'http://'`,
82
+		},
83
+		{
84
+			input:       "mirror-1.example.com:5000",
85
+			expectedErr: `invalid mirror: no scheme specified for "mirror-1.example.com:5000": must use either 'https://' or 'http://'`,
86
+		},
87
+		{
88
+			input:       "ftp://mirror-1.example.com",
89
+			expectedErr: `invalid mirror: unsupported scheme "ftp" in "ftp://mirror-1.example.com": must use either 'https://' or 'http://'`,
90
+		},
91
+		{
92
+			input:       "http://mirror-1.example.com/?q=foo",
93
+			expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/?q=foo"`,
94
+		},
95
+		{
96
+			input:       "http://mirror-1.example.com/v1/?q=foo",
97
+			expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/v1/?q=foo"`,
98
+		},
99
+		{
100
+			input:       "http://mirror-1.example.com/v1/?q=foo#frag",
101
+			expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/v1/?q=foo#frag"`,
102
+		},
103
+		{
104
+			input:       "http://mirror-1.example.com?q=foo",
105
+			expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com?q=foo"`,
106
+		},
107
+		{
108
+			input:       "https://mirror-1.example.com#frag",
109
+			expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com#frag"`,
110
+		},
111
+		{
112
+			input:       "https://mirror-1.example.com/#frag",
113
+			expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com/#frag"`,
114
+		},
115
+		{
116
+			input:       "http://foo:bar@mirror-1.example.com/",
117
+			expectedErr: `invalid mirror: username/password not allowed in URI "http://foo:xxxxx@mirror-1.example.com/"`,
118
+		},
119
+		{
120
+			input:       "https://mirror-1.example.com/v1/#frag",
121
+			expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com/v1/#frag"`,
122
+		},
123
+		{
124
+			input:       "https://mirror-1.example.com?q",
125
+			expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com?q"`,
126
+		},
127
+	}
128
+
129
+	for _, tc := range tests {
130
+		t.Run(tc.input, func(t *testing.T) {
131
+			out, err := ValidateMirror(tc.input)
132
+			if tc.expectedErr != "" {
133
+				assert.Error(t, err, tc.expectedErr)
134
+			} else {
135
+				assert.NilError(t, err)
136
+			}
137
+			assert.Check(t, is.Equal(out, tc.output))
138
+		})
139
+	}
140
+}
141
+
142
+func TestLoadInsecureRegistries(t *testing.T) {
143
+	testCases := []struct {
144
+		registries []string
145
+		index      string
146
+		err        string
147
+	}{
148
+		{
149
+			registries: []string{"127.0.0.1"},
150
+			index:      "127.0.0.1",
151
+		},
152
+		{
153
+			registries: []string{"127.0.0.1:8080"},
154
+			index:      "127.0.0.1:8080",
155
+		},
156
+		{
157
+			registries: []string{"2001:db8::1"},
158
+			index:      "2001:db8::1",
159
+		},
160
+		{
161
+			registries: []string{"[2001:db8::1]:80"},
162
+			index:      "[2001:db8::1]:80",
163
+		},
164
+		{
165
+			registries: []string{"http://myregistry.example.com"},
166
+			index:      "myregistry.example.com",
167
+		},
168
+		{
169
+			registries: []string{"https://myregistry.example.com"},
170
+			index:      "myregistry.example.com",
171
+		},
172
+		{
173
+			registries: []string{"HTTP://myregistry.example.com"},
174
+			index:      "myregistry.example.com",
175
+		},
176
+		{
177
+			registries: []string{"svn://myregistry.example.com"},
178
+			err:        "insecure registry svn://myregistry.example.com should not contain '://'",
179
+		},
180
+		{
181
+			registries: []string{"-invalid-registry"},
182
+			err:        "Cannot begin or end with a hyphen",
183
+		},
184
+		{
185
+			registries: []string{`mytest-.com`},
186
+			err:        `insecure registry mytest-.com is not valid: invalid host "mytest-.com"`,
187
+		},
188
+		{
189
+			registries: []string{`1200:0000:AB00:1234:0000:2552:7777:1313:8080`},
190
+			err:        `insecure registry 1200:0000:AB00:1234:0000:2552:7777:1313:8080 is not valid: invalid host "1200:0000:AB00:1234:0000:2552:7777:1313:8080"`,
191
+		},
192
+		{
193
+			registries: []string{`myregistry.example.com:500000`},
194
+			err:        `insecure registry myregistry.example.com:500000 is not valid: invalid port "500000"`,
195
+		},
196
+		{
197
+			registries: []string{`"myregistry.example.com"`},
198
+			err:        `insecure registry "myregistry.example.com" is not valid: invalid host "\"myregistry.example.com\""`,
199
+		},
200
+		{
201
+			registries: []string{`"myregistry.example.com:5000"`},
202
+			err:        `insecure registry "myregistry.example.com:5000" is not valid: invalid host "\"myregistry.example.com"`,
203
+		},
204
+	}
205
+	for _, testCase := range testCases {
206
+		config := &serviceConfig{}
207
+		err := config.loadInsecureRegistries(testCase.registries)
208
+		if testCase.err == "" {
209
+			if err != nil {
210
+				t.Fatalf("expect no error, got '%s'", err)
211
+			}
212
+			match := false
213
+			for index := range config.IndexConfigs {
214
+				if index == testCase.index {
215
+					match = true
216
+				}
217
+			}
218
+			if !match {
219
+				t.Fatalf("expect index configs to contain '%s', got %+v", testCase.index, config.IndexConfigs)
220
+			}
221
+		} else {
222
+			if err == nil {
223
+				t.Fatalf("expect error '%s', got no error", testCase.err)
224
+			}
225
+			assert.ErrorContains(t, err, testCase.err)
226
+			assert.Check(t, cerrdefs.IsInvalidArgument(err))
227
+		}
228
+	}
229
+}
230
+
231
+func TestNewServiceConfig(t *testing.T) {
232
+	tests := []struct {
233
+		doc    string
234
+		opts   ServiceOptions
235
+		errStr string
236
+	}{
237
+		{
238
+			doc: "empty config",
239
+		},
240
+		{
241
+			doc: "invalid mirror",
242
+			opts: ServiceOptions{
243
+				Mirrors: []string{"example.com:5000"},
244
+			},
245
+			errStr: `invalid mirror: no scheme specified for "example.com:5000": must use either 'https://' or 'http://'`,
246
+		},
247
+		{
248
+			doc: "valid mirror",
249
+			opts: ServiceOptions{
250
+				Mirrors: []string{"https://example.com:5000"},
251
+			},
252
+		},
253
+		{
254
+			doc: "invalid insecure registry",
255
+			opts: ServiceOptions{
256
+				InsecureRegistries: []string{"[fe80::]/64"},
257
+			},
258
+			errStr: `insecure registry [fe80::]/64 is not valid: invalid host "[fe80::]/64"`,
259
+		},
260
+		{
261
+			doc: "valid insecure registry",
262
+			opts: ServiceOptions{
263
+				InsecureRegistries: []string{"102.10.8.1/24"},
264
+			},
265
+		},
266
+	}
267
+
268
+	for _, tc := range tests {
269
+		t.Run(tc.doc, func(t *testing.T) {
270
+			_, err := newServiceConfig(tc.opts)
271
+			if tc.errStr != "" {
272
+				assert.Check(t, is.Error(err, tc.errStr))
273
+				assert.Check(t, cerrdefs.IsInvalidArgument(err))
274
+			} else {
275
+				assert.Check(t, err)
276
+			}
277
+		})
278
+	}
279
+}
280
+
281
+func TestValidateIndexName(t *testing.T) {
282
+	valid := []struct {
283
+		index  string
284
+		expect string
285
+	}{
286
+		{
287
+			index:  "index.docker.io",
288
+			expect: "docker.io",
289
+		},
290
+		{
291
+			index:  "example.com",
292
+			expect: "example.com",
293
+		},
294
+		{
295
+			index:  "127.0.0.1:8080",
296
+			expect: "127.0.0.1:8080",
297
+		},
298
+		{
299
+			index:  "mytest-1.com",
300
+			expect: "mytest-1.com",
301
+		},
302
+		{
303
+			index:  "mirror-1.example.com/v1/?q=foo",
304
+			expect: "mirror-1.example.com/v1/?q=foo",
305
+		},
306
+	}
307
+
308
+	for _, testCase := range valid {
309
+		result, err := ValidateIndexName(testCase.index)
310
+		if assert.Check(t, err) {
311
+			assert.Check(t, is.Equal(testCase.expect, result))
312
+		}
313
+	}
314
+}
315
+
316
+func TestValidateIndexNameWithError(t *testing.T) {
317
+	invalid := []struct {
318
+		index string
319
+		err   string
320
+	}{
321
+		{
322
+			index: "docker.io-",
323
+			err:   "invalid index name (docker.io-). Cannot begin or end with a hyphen",
324
+		},
325
+		{
326
+			index: "-example.com",
327
+			err:   "invalid index name (-example.com). Cannot begin or end with a hyphen",
328
+		},
329
+		{
330
+			index: "mirror-1.example.com/v1/?q=foo-",
331
+			err:   "invalid index name (mirror-1.example.com/v1/?q=foo-). Cannot begin or end with a hyphen",
332
+		},
333
+	}
334
+	for _, testCase := range invalid {
335
+		_, err := ValidateIndexName(testCase.index)
336
+		assert.Check(t, is.Error(err, testCase.err))
337
+		assert.Check(t, cerrdefs.IsInvalidArgument(err))
338
+	}
339
+}
0 340
new file mode 100644
... ...
@@ -0,0 +1,67 @@
0
+package registry
1
+
2
+import (
3
+	"net/url"
4
+
5
+	"github.com/docker/distribution/registry/api/errcode"
6
+	"github.com/pkg/errors"
7
+)
8
+
9
+func translateV2AuthError(err error) error {
10
+	var e *url.Error
11
+	if errors.As(err, &e) {
12
+		var e2 errcode.Error
13
+		if errors.As(e, &e2) && errors.Is(e2.Code, errcode.ErrorCodeUnauthorized) {
14
+			return unauthorizedErr{err}
15
+		}
16
+	}
17
+	return err
18
+}
19
+
20
+func invalidParam(err error) error {
21
+	return invalidParameterErr{err}
22
+}
23
+
24
+func invalidParamf(format string, args ...interface{}) error {
25
+	return invalidParameterErr{errors.Errorf(format, args...)}
26
+}
27
+
28
+func invalidParamWrapf(err error, format string, args ...interface{}) error {
29
+	return invalidParameterErr{errors.Wrapf(err, format, args...)}
30
+}
31
+
32
+type unauthorizedErr struct{ error }
33
+
34
+func (unauthorizedErr) Unauthorized() {}
35
+
36
+func (e unauthorizedErr) Cause() error {
37
+	return e.error
38
+}
39
+
40
+func (e unauthorizedErr) Unwrap() error {
41
+	return e.error
42
+}
43
+
44
+type invalidParameterErr struct{ error }
45
+
46
+func (invalidParameterErr) InvalidParameter() {}
47
+
48
+func (e invalidParameterErr) Unwrap() error {
49
+	return e.error
50
+}
51
+
52
+type systemErr struct{ error }
53
+
54
+func (systemErr) System() {}
55
+
56
+func (e systemErr) Unwrap() error {
57
+	return e.error
58
+}
59
+
60
+type errUnknown struct{ error }
61
+
62
+func (errUnknown) Unknown() {}
63
+
64
+func (e errUnknown) Unwrap() error {
65
+	return e.error
66
+}
0 67
new file mode 100644
... ...
@@ -0,0 +1,155 @@
0
+// Package registry contains client primitives to interact with a remote Docker registry.
1
+package registry
2
+
3
+import (
4
+	"context"
5
+	"crypto/tls"
6
+	"net"
7
+	"net/http"
8
+	"os"
9
+	"path/filepath"
10
+	"runtime"
11
+	"strings"
12
+	"time"
13
+
14
+	"github.com/containerd/log"
15
+	"github.com/docker/distribution/registry/client/transport"
16
+	"github.com/docker/go-connections/tlsconfig"
17
+	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
18
+)
19
+
20
+// hostCertsDir returns the config directory for a specific host.
21
+func hostCertsDir(hostnameAndPort string) string {
22
+	if runtime.GOOS == "windows" {
23
+		// Ensure that a directory name is valid; hostnameAndPort may contain
24
+		// a colon (:) if a port is included, and Windows does not allow colons
25
+		// in directory names.
26
+		hostnameAndPort = filepath.FromSlash(strings.ReplaceAll(hostnameAndPort, ":", ""))
27
+	}
28
+	return filepath.Join(CertsDir(), hostnameAndPort)
29
+}
30
+
31
+// newTLSConfig constructs a client TLS configuration based on server defaults
32
+func newTLSConfig(ctx context.Context, hostname string, isSecure bool) (*tls.Config, error) {
33
+	// PreferredServerCipherSuites should have no effect
34
+	tlsConfig := tlsconfig.ServerDefault()
35
+	tlsConfig.InsecureSkipVerify = !isSecure
36
+
37
+	if isSecure {
38
+		hostDir := hostCertsDir(hostname)
39
+		log.G(ctx).Debugf("hostDir: %s", hostDir)
40
+		if err := loadTLSConfig(ctx, hostDir, tlsConfig); err != nil {
41
+			return nil, err
42
+		}
43
+	}
44
+
45
+	return tlsConfig, nil
46
+}
47
+
48
+func hasFile(files []os.DirEntry, name string) bool {
49
+	for _, f := range files {
50
+		if f.Name() == name {
51
+			return true
52
+		}
53
+	}
54
+	return false
55
+}
56
+
57
+// ReadCertsDirectory reads the directory for TLS certificates
58
+// including roots and certificate pairs and updates the
59
+// provided TLS configuration.
60
+func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
61
+	return loadTLSConfig(context.TODO(), directory, tlsConfig)
62
+}
63
+
64
+// loadTLSConfig reads the directory for TLS certificates including roots and
65
+// certificate pairs, and updates the provided TLS configuration.
66
+func loadTLSConfig(ctx context.Context, directory string, tlsConfig *tls.Config) error {
67
+	fs, err := os.ReadDir(directory)
68
+	if err != nil {
69
+		if os.IsNotExist(err) {
70
+			return nil
71
+		}
72
+		return invalidParam(err)
73
+	}
74
+
75
+	for _, f := range fs {
76
+		if ctx.Err() != nil {
77
+			return ctx.Err()
78
+		}
79
+		switch filepath.Ext(f.Name()) {
80
+		case ".crt":
81
+			if tlsConfig.RootCAs == nil {
82
+				systemPool, err := tlsconfig.SystemCertPool()
83
+				if err != nil {
84
+					return invalidParamWrapf(err, "unable to get system cert pool")
85
+				}
86
+				tlsConfig.RootCAs = systemPool
87
+			}
88
+			fileName := filepath.Join(directory, f.Name())
89
+			log.G(ctx).Debugf("crt: %s", fileName)
90
+			data, err := os.ReadFile(fileName)
91
+			if err != nil {
92
+				return err
93
+			}
94
+			tlsConfig.RootCAs.AppendCertsFromPEM(data)
95
+		case ".cert":
96
+			certName := f.Name()
97
+			keyName := certName[:len(certName)-5] + ".key"
98
+			log.G(ctx).Debugf("cert: %s", filepath.Join(directory, certName))
99
+			if !hasFile(fs, keyName) {
100
+				return invalidParamf("missing key %s for client certificate %s. CA certificates must use the extension .crt", keyName, certName)
101
+			}
102
+			cert, err := tls.LoadX509KeyPair(filepath.Join(directory, certName), filepath.Join(directory, keyName))
103
+			if err != nil {
104
+				return err
105
+			}
106
+			tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
107
+		case ".key":
108
+			keyName := f.Name()
109
+			certName := keyName[:len(keyName)-4] + ".cert"
110
+			log.G(ctx).Debugf("key: %s", filepath.Join(directory, keyName))
111
+			if !hasFile(fs, certName) {
112
+				return invalidParamf("missing client certificate %s for key %s", certName, keyName)
113
+			}
114
+		}
115
+	}
116
+
117
+	return nil
118
+}
119
+
120
+// Headers returns request modifiers with a User-Agent and metaHeaders
121
+func Headers(userAgent string, metaHeaders http.Header) []transport.RequestModifier {
122
+	modifiers := []transport.RequestModifier{}
123
+	if userAgent != "" {
124
+		modifiers = append(modifiers, transport.NewHeaderRequestModifier(http.Header{
125
+			"User-Agent": []string{userAgent},
126
+		}))
127
+	}
128
+	if metaHeaders != nil {
129
+		modifiers = append(modifiers, transport.NewHeaderRequestModifier(metaHeaders))
130
+	}
131
+	return modifiers
132
+}
133
+
134
+// newTransport returns a new HTTP transport. If tlsConfig is nil, it uses the
135
+// default TLS configuration.
136
+func newTransport(tlsConfig *tls.Config) http.RoundTripper {
137
+	if tlsConfig == nil {
138
+		tlsConfig = tlsconfig.ServerDefault()
139
+	}
140
+
141
+	return otelhttp.NewTransport(
142
+		&http.Transport{
143
+			Proxy: http.ProxyFromEnvironment,
144
+			DialContext: (&net.Dialer{
145
+				Timeout:   30 * time.Second,
146
+				KeepAlive: 30 * time.Second,
147
+			}).DialContext,
148
+			TLSHandshakeTimeout: 10 * time.Second,
149
+			TLSClientConfig:     tlsConfig,
150
+			// TODO(dmcgowan): Call close idle connections when complete and use keep alive
151
+			DisableKeepAlives: true,
152
+		},
153
+	)
154
+}
0 155
new file mode 100644
... ...
@@ -0,0 +1,120 @@
0
+package registry
1
+
2
+import (
3
+	"context"
4
+	"encoding/json"
5
+	"io"
6
+	"net/http"
7
+	"net/http/httptest"
8
+	"testing"
9
+
10
+	"github.com/containerd/log"
11
+	"github.com/moby/moby/api/types/registry"
12
+	"gotest.tools/v3/assert"
13
+)
14
+
15
+var (
16
+	testHTTPServer  *httptest.Server
17
+	testHTTPSServer *httptest.Server
18
+)
19
+
20
+func init() {
21
+	r := http.NewServeMux()
22
+
23
+	// /v1/
24
+	r.HandleFunc("/v1/_ping", handlerGetPing)
25
+	r.HandleFunc("/v1/search", handlerSearch)
26
+
27
+	// /v2/
28
+	r.HandleFunc("/v2/version", handlerGetPing)
29
+
30
+	testHTTPServer = httptest.NewServer(handlerAccessLog(r))
31
+	testHTTPSServer = httptest.NewTLSServer(handlerAccessLog(r))
32
+}
33
+
34
+func handlerAccessLog(handler http.Handler) http.Handler {
35
+	logHandler := func(w http.ResponseWriter, r *http.Request) {
36
+		log.G(context.TODO()).Debugf(`%s "%s %s"`, r.RemoteAddr, r.Method, r.URL)
37
+		handler.ServeHTTP(w, r)
38
+	}
39
+	return http.HandlerFunc(logHandler)
40
+}
41
+
42
+func makeURL(req string) string {
43
+	return testHTTPServer.URL + req
44
+}
45
+
46
+func makeHTTPSURL(req string) string {
47
+	return testHTTPSServer.URL + req
48
+}
49
+
50
+func makeIndex(req string) *registry.IndexInfo {
51
+	return &registry.IndexInfo{
52
+		Name: makeURL(req),
53
+	}
54
+}
55
+
56
+func makeHTTPSIndex(req string) *registry.IndexInfo {
57
+	return &registry.IndexInfo{
58
+		Name: makeHTTPSURL(req),
59
+	}
60
+}
61
+
62
+func makePublicIndex() *registry.IndexInfo {
63
+	return &registry.IndexInfo{
64
+		Name:     IndexServer,
65
+		Secure:   true,
66
+		Official: true,
67
+	}
68
+}
69
+
70
+func writeHeaders(w http.ResponseWriter) {
71
+	h := w.Header()
72
+	h.Add("Server", "docker-tests/mock")
73
+	h.Add("Expires", "-1")
74
+	h.Add("Content-Type", "application/json")
75
+	h.Add("Pragma", "no-cache")
76
+	h.Add("Cache-Control", "no-cache")
77
+}
78
+
79
+func writeResponse(w http.ResponseWriter, message interface{}, code int) {
80
+	writeHeaders(w)
81
+	w.WriteHeader(code)
82
+	body, err := json.Marshal(message)
83
+	if err != nil {
84
+		_, _ = io.WriteString(w, err.Error())
85
+		return
86
+	}
87
+	_, _ = w.Write(body)
88
+}
89
+
90
+func handlerGetPing(w http.ResponseWriter, r *http.Request) {
91
+	if r.Method != http.MethodGet {
92
+		writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
93
+		return
94
+	}
95
+	writeResponse(w, true, http.StatusOK)
96
+}
97
+
98
+func handlerSearch(w http.ResponseWriter, r *http.Request) {
99
+	if r.Method != http.MethodGet {
100
+		writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
101
+		return
102
+	}
103
+	result := &registry.SearchResults{
104
+		Query:      "fakequery",
105
+		NumResults: 1,
106
+		Results:    []registry.SearchResult{{Name: "fakeimage", StarCount: 42}},
107
+	}
108
+	writeResponse(w, result, http.StatusOK)
109
+}
110
+
111
+func TestPing(t *testing.T) {
112
+	res, err := http.Get(makeURL("/v1/_ping"))
113
+	if err != nil {
114
+		t.Fatal(err)
115
+	}
116
+	assert.Equal(t, res.StatusCode, http.StatusOK, "")
117
+	assert.Equal(t, res.Header.Get("Server"), "docker-tests/mock")
118
+	_ = res.Body.Close()
119
+}
0 120
new file mode 100644
... ...
@@ -0,0 +1,637 @@
0
+package registry
1
+
2
+import (
3
+	"errors"
4
+	"net"
5
+	"testing"
6
+
7
+	"github.com/distribution/reference"
8
+	"github.com/moby/moby/api/types/registry"
9
+	"gotest.tools/v3/assert"
10
+	is "gotest.tools/v3/assert/cmp"
11
+)
12
+
13
+// overrideLookupIP overrides net.LookupIP for testing.
14
+func overrideLookupIP(t *testing.T) {
15
+	t.Helper()
16
+	restoreLookup := lookupIP
17
+
18
+	// override net.LookupIP
19
+	lookupIP = func(host string) ([]net.IP, error) {
20
+		mockHosts := map[string][]net.IP{
21
+			"":            {net.ParseIP("0.0.0.0")},
22
+			"localhost":   {net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
23
+			"example.com": {net.ParseIP("42.42.42.42")},
24
+			"other.com":   {net.ParseIP("43.43.43.43")},
25
+		}
26
+		if addrs, ok := mockHosts[host]; ok {
27
+			return addrs, nil
28
+		}
29
+		return nil, errors.New("lookup: no such host")
30
+	}
31
+	t.Cleanup(func() {
32
+		lookupIP = restoreLookup
33
+	})
34
+}
35
+
36
+func TestParseRepositoryInfo(t *testing.T) {
37
+	type staticRepositoryInfo struct {
38
+		Index         *registry.IndexInfo
39
+		RemoteName    string
40
+		CanonicalName string
41
+		LocalName     string
42
+	}
43
+
44
+	tests := map[string]staticRepositoryInfo{
45
+		"fooo/bar": {
46
+			Index: &registry.IndexInfo{
47
+				Name:     IndexName,
48
+				Mirrors:  []string{},
49
+				Official: true,
50
+				Secure:   true,
51
+			},
52
+			RemoteName:    "fooo/bar",
53
+			LocalName:     "fooo/bar",
54
+			CanonicalName: "docker.io/fooo/bar",
55
+		},
56
+		"library/ubuntu": {
57
+			Index: &registry.IndexInfo{
58
+				Name:     IndexName,
59
+				Mirrors:  []string{},
60
+				Official: true,
61
+				Secure:   true,
62
+			},
63
+			RemoteName:    "library/ubuntu",
64
+			LocalName:     "ubuntu",
65
+			CanonicalName: "docker.io/library/ubuntu",
66
+		},
67
+		"nonlibrary/ubuntu": {
68
+			Index: &registry.IndexInfo{
69
+				Name:     IndexName,
70
+				Mirrors:  []string{},
71
+				Official: true,
72
+				Secure:   true,
73
+			},
74
+			RemoteName:    "nonlibrary/ubuntu",
75
+			LocalName:     "nonlibrary/ubuntu",
76
+			CanonicalName: "docker.io/nonlibrary/ubuntu",
77
+		},
78
+		"ubuntu": {
79
+			Index: &registry.IndexInfo{
80
+				Name:     IndexName,
81
+				Mirrors:  []string{},
82
+				Official: true,
83
+				Secure:   true,
84
+			},
85
+			RemoteName:    "library/ubuntu",
86
+			LocalName:     "ubuntu",
87
+			CanonicalName: "docker.io/library/ubuntu",
88
+		},
89
+		"other/library": {
90
+			Index: &registry.IndexInfo{
91
+				Name:     IndexName,
92
+				Mirrors:  []string{},
93
+				Official: true,
94
+				Secure:   true,
95
+			},
96
+			RemoteName:    "other/library",
97
+			LocalName:     "other/library",
98
+			CanonicalName: "docker.io/other/library",
99
+		},
100
+		"127.0.0.1:8000/private/moonbase": {
101
+			Index: &registry.IndexInfo{
102
+				Name:     "127.0.0.1:8000",
103
+				Mirrors:  []string{},
104
+				Official: false,
105
+				Secure:   false,
106
+			},
107
+			RemoteName:    "private/moonbase",
108
+			LocalName:     "127.0.0.1:8000/private/moonbase",
109
+			CanonicalName: "127.0.0.1:8000/private/moonbase",
110
+		},
111
+		"127.0.0.1:8000/privatebase": {
112
+			Index: &registry.IndexInfo{
113
+				Name:     "127.0.0.1:8000",
114
+				Mirrors:  []string{},
115
+				Official: false,
116
+				Secure:   false,
117
+			},
118
+			RemoteName:    "privatebase",
119
+			LocalName:     "127.0.0.1:8000/privatebase",
120
+			CanonicalName: "127.0.0.1:8000/privatebase",
121
+		},
122
+		"[::1]:8000/private/moonbase": {
123
+			Index: &registry.IndexInfo{
124
+				Name:     "[::1]:8000",
125
+				Mirrors:  []string{},
126
+				Official: false,
127
+				Secure:   false,
128
+			},
129
+			RemoteName:    "private/moonbase",
130
+			LocalName:     "[::1]:8000/private/moonbase",
131
+			CanonicalName: "[::1]:8000/private/moonbase",
132
+		},
133
+		"[::1]:8000/privatebase": {
134
+			Index: &registry.IndexInfo{
135
+				Name:     "[::1]:8000",
136
+				Mirrors:  []string{},
137
+				Official: false,
138
+				Secure:   false,
139
+			},
140
+			RemoteName:    "privatebase",
141
+			LocalName:     "[::1]:8000/privatebase",
142
+			CanonicalName: "[::1]:8000/privatebase",
143
+		},
144
+		// IPv6 only has a single loopback address, so ::2 is not a loopback,
145
+		// hence not marked "insecure".
146
+		"[::2]:8000/private/moonbase": {
147
+			Index: &registry.IndexInfo{
148
+				Name:     "[::2]:8000",
149
+				Mirrors:  []string{},
150
+				Official: false,
151
+				Secure:   true,
152
+			},
153
+			RemoteName:    "private/moonbase",
154
+			LocalName:     "[::2]:8000/private/moonbase",
155
+			CanonicalName: "[::2]:8000/private/moonbase",
156
+		},
157
+		// IPv6 only has a single loopback address, so ::2 is not a loopback,
158
+		// hence not marked "insecure".
159
+		"[::2]:8000/privatebase": {
160
+			Index: &registry.IndexInfo{
161
+				Name:     "[::2]:8000",
162
+				Mirrors:  []string{},
163
+				Official: false,
164
+				Secure:   true,
165
+			},
166
+			RemoteName:    "privatebase",
167
+			LocalName:     "[::2]:8000/privatebase",
168
+			CanonicalName: "[::2]:8000/privatebase",
169
+		},
170
+		"localhost:8000/private/moonbase": {
171
+			Index: &registry.IndexInfo{
172
+				Name:     "localhost:8000",
173
+				Mirrors:  []string{},
174
+				Official: false,
175
+				Secure:   false,
176
+			},
177
+			RemoteName:    "private/moonbase",
178
+			LocalName:     "localhost:8000/private/moonbase",
179
+			CanonicalName: "localhost:8000/private/moonbase",
180
+		},
181
+		"localhost:8000/privatebase": {
182
+			Index: &registry.IndexInfo{
183
+				Name:     "localhost:8000",
184
+				Mirrors:  []string{},
185
+				Official: false,
186
+				Secure:   false,
187
+			},
188
+			RemoteName:    "privatebase",
189
+			LocalName:     "localhost:8000/privatebase",
190
+			CanonicalName: "localhost:8000/privatebase",
191
+		},
192
+		"example.com/private/moonbase": {
193
+			Index: &registry.IndexInfo{
194
+				Name:     "example.com",
195
+				Mirrors:  []string{},
196
+				Official: false,
197
+				Secure:   true,
198
+			},
199
+			RemoteName:    "private/moonbase",
200
+			LocalName:     "example.com/private/moonbase",
201
+			CanonicalName: "example.com/private/moonbase",
202
+		},
203
+		"example.com/privatebase": {
204
+			Index: &registry.IndexInfo{
205
+				Name:     "example.com",
206
+				Mirrors:  []string{},
207
+				Official: false,
208
+				Secure:   true,
209
+			},
210
+			RemoteName:    "privatebase",
211
+			LocalName:     "example.com/privatebase",
212
+			CanonicalName: "example.com/privatebase",
213
+		},
214
+		"example.com:8000/private/moonbase": {
215
+			Index: &registry.IndexInfo{
216
+				Name:     "example.com:8000",
217
+				Mirrors:  []string{},
218
+				Official: false,
219
+				Secure:   true,
220
+			},
221
+			RemoteName:    "private/moonbase",
222
+			LocalName:     "example.com:8000/private/moonbase",
223
+			CanonicalName: "example.com:8000/private/moonbase",
224
+		},
225
+		"example.com:8000/privatebase": {
226
+			Index: &registry.IndexInfo{
227
+				Name:     "example.com:8000",
228
+				Mirrors:  []string{},
229
+				Official: false,
230
+				Secure:   true,
231
+			},
232
+			RemoteName:    "privatebase",
233
+			LocalName:     "example.com:8000/privatebase",
234
+			CanonicalName: "example.com:8000/privatebase",
235
+		},
236
+		"localhost/private/moonbase": {
237
+			Index: &registry.IndexInfo{
238
+				Name:     "localhost",
239
+				Mirrors:  []string{},
240
+				Official: false,
241
+				Secure:   false,
242
+			},
243
+			RemoteName:    "private/moonbase",
244
+			LocalName:     "localhost/private/moonbase",
245
+			CanonicalName: "localhost/private/moonbase",
246
+		},
247
+		"localhost/privatebase": {
248
+			Index: &registry.IndexInfo{
249
+				Name:     "localhost",
250
+				Mirrors:  []string{},
251
+				Official: false,
252
+				Secure:   false,
253
+			},
254
+			RemoteName:    "privatebase",
255
+			LocalName:     "localhost/privatebase",
256
+			CanonicalName: "localhost/privatebase",
257
+		},
258
+		IndexName + "/public/moonbase": {
259
+			Index: &registry.IndexInfo{
260
+				Name:     IndexName,
261
+				Mirrors:  []string{},
262
+				Official: true,
263
+				Secure:   true,
264
+			},
265
+			RemoteName:    "public/moonbase",
266
+			LocalName:     "public/moonbase",
267
+			CanonicalName: "docker.io/public/moonbase",
268
+		},
269
+		"index." + IndexName + "/public/moonbase": {
270
+			Index: &registry.IndexInfo{
271
+				Name:     IndexName,
272
+				Mirrors:  []string{},
273
+				Official: true,
274
+				Secure:   true,
275
+			},
276
+			RemoteName:    "public/moonbase",
277
+			LocalName:     "public/moonbase",
278
+			CanonicalName: "docker.io/public/moonbase",
279
+		},
280
+		"ubuntu-12.04-base": {
281
+			Index: &registry.IndexInfo{
282
+				Name:     IndexName,
283
+				Mirrors:  []string{},
284
+				Official: true,
285
+				Secure:   true,
286
+			},
287
+			RemoteName:    "library/ubuntu-12.04-base",
288
+			LocalName:     "ubuntu-12.04-base",
289
+			CanonicalName: "docker.io/library/ubuntu-12.04-base",
290
+		},
291
+		IndexName + "/ubuntu-12.04-base": {
292
+			Index: &registry.IndexInfo{
293
+				Name:     IndexName,
294
+				Mirrors:  []string{},
295
+				Official: true,
296
+				Secure:   true,
297
+			},
298
+			RemoteName:    "library/ubuntu-12.04-base",
299
+			LocalName:     "ubuntu-12.04-base",
300
+			CanonicalName: "docker.io/library/ubuntu-12.04-base",
301
+		},
302
+		"index." + IndexName + "/ubuntu-12.04-base": {
303
+			Index: &registry.IndexInfo{
304
+				Name:     IndexName,
305
+				Mirrors:  []string{},
306
+				Official: true,
307
+				Secure:   true,
308
+			},
309
+			RemoteName:    "library/ubuntu-12.04-base",
310
+			LocalName:     "ubuntu-12.04-base",
311
+			CanonicalName: "docker.io/library/ubuntu-12.04-base",
312
+		},
313
+	}
314
+
315
+	for reposName, expected := range tests {
316
+		t.Run(reposName, func(t *testing.T) {
317
+			named, err := reference.ParseNormalizedNamed(reposName)
318
+			assert.NilError(t, err)
319
+
320
+			repoInfo, err := ParseRepositoryInfo(named)
321
+			assert.NilError(t, err)
322
+
323
+			assert.Check(t, is.DeepEqual(repoInfo.Index, expected.Index))
324
+			assert.Check(t, is.Equal(reference.Path(repoInfo.Name), expected.RemoteName))
325
+			assert.Check(t, is.Equal(reference.FamiliarName(repoInfo.Name), expected.LocalName))
326
+			assert.Check(t, is.Equal(repoInfo.Name.Name(), expected.CanonicalName))
327
+		})
328
+	}
329
+}
330
+
331
+func TestNewIndexInfo(t *testing.T) {
332
+	overrideLookupIP(t)
333
+
334
+	// ipv6Loopback is the CIDR for the IPv6 loopback address ("::1"); "::1/128"
335
+	ipv6Loopback := &net.IPNet{
336
+		IP:   net.IPv6loopback,
337
+		Mask: net.CIDRMask(128, 128),
338
+	}
339
+
340
+	// ipv4Loopback is the CIDR for IPv4 loopback addresses ("127.0.0.0/8")
341
+	ipv4Loopback := &net.IPNet{
342
+		IP:   net.IPv4(127, 0, 0, 0),
343
+		Mask: net.CIDRMask(8, 32),
344
+	}
345
+
346
+	// emptyServiceConfig is a default service-config for situations where
347
+	// no config-file is available (e.g. when used in the CLI). It won't
348
+	// have mirrors configured, but does have the default insecure registry
349
+	// CIDRs for loopback interfaces configured.
350
+	emptyServiceConfig := &serviceConfig{
351
+		IndexConfigs: map[string]*registry.IndexInfo{
352
+			IndexName: {
353
+				Name:     IndexName,
354
+				Mirrors:  []string{},
355
+				Secure:   true,
356
+				Official: true,
357
+			},
358
+		},
359
+		InsecureRegistryCIDRs: []*registry.NetIPNet{
360
+			(*registry.NetIPNet)(ipv6Loopback),
361
+			(*registry.NetIPNet)(ipv4Loopback),
362
+		},
363
+	}
364
+
365
+	expectedIndexInfos := map[string]*registry.IndexInfo{
366
+		IndexName: {
367
+			Name:     IndexName,
368
+			Official: true,
369
+			Secure:   true,
370
+			Mirrors:  []string{},
371
+		},
372
+		"index." + IndexName: {
373
+			Name:     IndexName,
374
+			Official: true,
375
+			Secure:   true,
376
+			Mirrors:  []string{},
377
+		},
378
+		"example.com": {
379
+			Name:     "example.com",
380
+			Official: false,
381
+			Secure:   true,
382
+			Mirrors:  []string{},
383
+		},
384
+		"127.0.0.1:5000": {
385
+			Name:     "127.0.0.1:5000",
386
+			Official: false,
387
+			Secure:   false,
388
+			Mirrors:  []string{},
389
+		},
390
+	}
391
+	t.Run("no mirrors", func(t *testing.T) {
392
+		for indexName, expected := range expectedIndexInfos {
393
+			t.Run(indexName, func(t *testing.T) {
394
+				actual := newIndexInfo(emptyServiceConfig, indexName)
395
+				assert.Check(t, is.DeepEqual(actual, expected))
396
+			})
397
+		}
398
+	})
399
+
400
+	expectedIndexInfos = map[string]*registry.IndexInfo{
401
+		IndexName: {
402
+			Name:     IndexName,
403
+			Official: true,
404
+			Secure:   true,
405
+			Mirrors:  []string{"http://mirror1.local/", "http://mirror2.local/"},
406
+		},
407
+		"index." + IndexName: {
408
+			Name:     IndexName,
409
+			Official: true,
410
+			Secure:   true,
411
+			Mirrors:  []string{"http://mirror1.local/", "http://mirror2.local/"},
412
+		},
413
+		"example.com": {
414
+			Name:     "example.com",
415
+			Official: false,
416
+			Secure:   false,
417
+			Mirrors:  []string{},
418
+		},
419
+		"example.com:5000": {
420
+			Name:     "example.com:5000",
421
+			Official: false,
422
+			Secure:   true,
423
+			Mirrors:  []string{},
424
+		},
425
+		"127.0.0.1": {
426
+			Name:     "127.0.0.1",
427
+			Official: false,
428
+			Secure:   false,
429
+			Mirrors:  []string{},
430
+		},
431
+		"127.0.0.1:5000": {
432
+			Name:     "127.0.0.1:5000",
433
+			Official: false,
434
+			Secure:   false,
435
+			Mirrors:  []string{},
436
+		},
437
+		"127.255.255.255": {
438
+			Name:     "127.255.255.255",
439
+			Official: false,
440
+			Secure:   false,
441
+			Mirrors:  []string{},
442
+		},
443
+		"127.255.255.255:5000": {
444
+			Name:     "127.255.255.255:5000",
445
+			Official: false,
446
+			Secure:   false,
447
+			Mirrors:  []string{},
448
+		},
449
+		"::1": {
450
+			Name:     "::1",
451
+			Official: false,
452
+			Secure:   false,
453
+			Mirrors:  []string{},
454
+		},
455
+		"[::1]:5000": {
456
+			Name:     "[::1]:5000",
457
+			Official: false,
458
+			Secure:   false,
459
+			Mirrors:  []string{},
460
+		},
461
+		// IPv6 only has a single loopback address, so ::2 is not a loopback,
462
+		// hence not marked "insecure".
463
+		"::2": {
464
+			Name:     "::2",
465
+			Official: false,
466
+			Secure:   true,
467
+			Mirrors:  []string{},
468
+		},
469
+		// IPv6 only has a single loopback address, so ::2 is not a loopback,
470
+		// hence not marked "insecure".
471
+		"[::2]:5000": {
472
+			Name:     "[::2]:5000",
473
+			Official: false,
474
+			Secure:   true,
475
+			Mirrors:  []string{},
476
+		},
477
+		"other.com": {
478
+			Name:     "other.com",
479
+			Official: false,
480
+			Secure:   true,
481
+			Mirrors:  []string{},
482
+		},
483
+	}
484
+	t.Run("mirrors", func(t *testing.T) {
485
+		// Note that newServiceConfig calls ValidateMirror internally, which normalizes
486
+		// mirror-URLs to have a trailing slash.
487
+		config, err := newServiceConfig(ServiceOptions{
488
+			Mirrors:            []string{"http://mirror1.local", "http://mirror2.local"},
489
+			InsecureRegistries: []string{"example.com"},
490
+		})
491
+		assert.NilError(t, err)
492
+		for indexName, expected := range expectedIndexInfos {
493
+			t.Run(indexName, func(t *testing.T) {
494
+				actual := newIndexInfo(config, indexName)
495
+				assert.Check(t, is.DeepEqual(actual, expected))
496
+			})
497
+		}
498
+	})
499
+
500
+	expectedIndexInfos = map[string]*registry.IndexInfo{
501
+		"example.com": {
502
+			Name:     "example.com",
503
+			Official: false,
504
+			Secure:   false,
505
+			Mirrors:  []string{},
506
+		},
507
+		"example.com:5000": {
508
+			Name:     "example.com:5000",
509
+			Official: false,
510
+			Secure:   false,
511
+			Mirrors:  []string{},
512
+		},
513
+		"127.0.0.1": {
514
+			Name:     "127.0.0.1",
515
+			Official: false,
516
+			Secure:   false,
517
+			Mirrors:  []string{},
518
+		},
519
+		"127.0.0.1:5000": {
520
+			Name:     "127.0.0.1:5000",
521
+			Official: false,
522
+			Secure:   false,
523
+			Mirrors:  []string{},
524
+		},
525
+		"42.42.0.1:5000": {
526
+			Name:     "42.42.0.1:5000",
527
+			Official: false,
528
+			Secure:   false,
529
+			Mirrors:  []string{},
530
+		},
531
+		"42.43.0.1:5000": {
532
+			Name:     "42.43.0.1:5000",
533
+			Official: false,
534
+			Secure:   true,
535
+			Mirrors:  []string{},
536
+		},
537
+		"other.com": {
538
+			Name:     "other.com",
539
+			Official: false,
540
+			Secure:   true,
541
+			Mirrors:  []string{},
542
+		},
543
+	}
544
+	t.Run("custom insecure", func(t *testing.T) {
545
+		config, err := newServiceConfig(ServiceOptions{
546
+			InsecureRegistries: []string{"42.42.0.0/16"},
547
+		})
548
+		assert.NilError(t, err)
549
+		for indexName, expected := range expectedIndexInfos {
550
+			t.Run(indexName, func(t *testing.T) {
551
+				actual := newIndexInfo(config, indexName)
552
+				assert.Check(t, is.DeepEqual(actual, expected))
553
+			})
554
+		}
555
+	})
556
+}
557
+
558
+func TestMirrorEndpointLookup(t *testing.T) {
559
+	containsMirror := func(endpoints []APIEndpoint) bool {
560
+		for _, pe := range endpoints {
561
+			if pe.URL.Host == "my.mirror" {
562
+				return true
563
+			}
564
+		}
565
+		return false
566
+	}
567
+	cfg, err := newServiceConfig(ServiceOptions{
568
+		Mirrors: []string{"https://my.mirror"},
569
+	})
570
+	assert.NilError(t, err)
571
+	s := Service{config: cfg}
572
+
573
+	imageName, err := reference.WithName(IndexName + "/test/image")
574
+	if err != nil {
575
+		t.Error(err)
576
+	}
577
+	pushAPIEndpoints, err := s.LookupPushEndpoints(reference.Domain(imageName))
578
+	if err != nil {
579
+		t.Fatal(err)
580
+	}
581
+	if containsMirror(pushAPIEndpoints) {
582
+		t.Fatal("Push endpoint should not contain mirror")
583
+	}
584
+
585
+	pullAPIEndpoints, err := s.LookupPullEndpoints(reference.Domain(imageName))
586
+	if err != nil {
587
+		t.Fatal(err)
588
+	}
589
+	if !containsMirror(pullAPIEndpoints) {
590
+		t.Fatal("Pull endpoint should contain mirror")
591
+	}
592
+}
593
+
594
+func TestIsSecureIndex(t *testing.T) {
595
+	overrideLookupIP(t)
596
+	tests := []struct {
597
+		addr               string
598
+		insecureRegistries []string
599
+		expected           bool
600
+	}{
601
+		{IndexName, nil, true},
602
+		{"example.com", []string{}, true},
603
+		{"example.com", []string{"example.com"}, false},
604
+		{"localhost", []string{"localhost:5000"}, false},
605
+		{"localhost:5000", []string{"localhost:5000"}, false},
606
+		{"localhost", []string{"example.com"}, false},
607
+		{"127.0.0.1:5000", []string{"127.0.0.1:5000"}, false},
608
+		{"localhost", nil, false},
609
+		{"localhost:5000", nil, false},
610
+		{"127.0.0.1", nil, false},
611
+		{"localhost", []string{"example.com"}, false},
612
+		{"127.0.0.1", []string{"example.com"}, false},
613
+		{"example.com", nil, true},
614
+		{"example.com", []string{"example.com"}, false},
615
+		{"127.0.0.1", []string{"example.com"}, false},
616
+		{"127.0.0.1:5000", []string{"example.com"}, false},
617
+		{"example.com:5000", []string{"42.42.0.0/16"}, false},
618
+		{"example.com", []string{"42.42.0.0/16"}, false},
619
+		{"example.com:5000", []string{"42.42.42.42/8"}, false},
620
+		{"127.0.0.1:5000", []string{"127.0.0.0/8"}, false},
621
+		{"42.42.42.42:5000", []string{"42.1.1.1/8"}, false},
622
+		{"invalid.example.com", []string{"42.42.0.0/16"}, true},
623
+		{"invalid.example.com", []string{"invalid.example.com"}, false},
624
+		{"invalid.example.com:5000", []string{"invalid.example.com"}, true},
625
+		{"invalid.example.com:5000", []string{"invalid.example.com:5000"}, false},
626
+	}
627
+	for _, tc := range tests {
628
+		config, err := newServiceConfig(ServiceOptions{
629
+			InsecureRegistries: tc.insecureRegistries,
630
+		})
631
+		assert.NilError(t, err)
632
+
633
+		sec := config.isSecureIndex(tc.addr)
634
+		assert.Equal(t, sec, tc.expected, "isSecureIndex failed for %q %v, expected %v got %v", tc.addr, tc.insecureRegistries, tc.expected, sec)
635
+	}
636
+}
0 637
new file mode 100644
... ...
@@ -0,0 +1,100 @@
0
+package resumable
1
+
2
+import (
3
+	"context"
4
+	"errors"
5
+	"fmt"
6
+	"io"
7
+	"net/http"
8
+	"time"
9
+
10
+	"github.com/containerd/log"
11
+)
12
+
13
+type requestReader struct {
14
+	client          *http.Client
15
+	request         *http.Request
16
+	lastRange       int64
17
+	totalSize       int64
18
+	currentResponse *http.Response
19
+	failures        uint32
20
+	maxFailures     uint32
21
+	waitDuration    time.Duration
22
+}
23
+
24
+// NewRequestReader makes it possible to resume reading a request's body transparently
25
+// maxfail is the number of times we retry to make requests again (not resumes)
26
+// totalsize is the total length of the body; auto detect if not provided
27
+func NewRequestReader(c *http.Client, r *http.Request, maxfail uint32, totalsize int64) io.ReadCloser {
28
+	return &requestReader{client: c, request: r, maxFailures: maxfail, totalSize: totalsize, waitDuration: 5 * time.Second}
29
+}
30
+
31
+// NewRequestReaderWithInitialResponse makes it possible to resume
32
+// reading the body of an already initiated request.
33
+func NewRequestReaderWithInitialResponse(c *http.Client, r *http.Request, maxfail uint32, totalsize int64, initialResponse *http.Response) io.ReadCloser {
34
+	return &requestReader{client: c, request: r, maxFailures: maxfail, totalSize: totalsize, currentResponse: initialResponse, waitDuration: 5 * time.Second}
35
+}
36
+
37
+func (r *requestReader) Read(p []byte) (n int, _ error) {
38
+	if r.client == nil || r.request == nil {
39
+		return 0, errors.New("client and request can't be nil")
40
+	}
41
+
42
+	var err error
43
+	isFreshRequest := false
44
+	if r.lastRange != 0 && r.currentResponse == nil {
45
+		readRange := fmt.Sprintf("bytes=%d-%d", r.lastRange, r.totalSize)
46
+		r.request.Header.Set("Range", readRange)
47
+		time.Sleep(r.waitDuration)
48
+	}
49
+	if r.currentResponse == nil {
50
+		r.currentResponse, err = r.client.Do(r.request)
51
+		isFreshRequest = true
52
+	}
53
+	if err != nil && r.failures+1 != r.maxFailures {
54
+		r.cleanUpResponse()
55
+		r.failures++
56
+		time.Sleep(time.Duration(r.failures) * r.waitDuration)
57
+		return 0, nil
58
+	} else if err != nil {
59
+		r.cleanUpResponse()
60
+		return 0, err
61
+	}
62
+	if r.currentResponse.StatusCode == http.StatusRequestedRangeNotSatisfiable && r.lastRange == r.totalSize && r.currentResponse.ContentLength == 0 {
63
+		r.cleanUpResponse()
64
+		return 0, io.EOF
65
+	} else if r.currentResponse.StatusCode != http.StatusPartialContent && r.lastRange != 0 && isFreshRequest {
66
+		r.cleanUpResponse()
67
+		return 0, errors.New("the server doesn't support byte ranges")
68
+	}
69
+	if r.totalSize == 0 {
70
+		r.totalSize = r.currentResponse.ContentLength
71
+	} else if r.totalSize <= 0 {
72
+		r.cleanUpResponse()
73
+		return 0, errors.New("failed to auto detect content length")
74
+	}
75
+	n, err = r.currentResponse.Body.Read(p)
76
+	r.lastRange += int64(n)
77
+	if err != nil {
78
+		r.cleanUpResponse()
79
+	}
80
+	if err != nil && !errors.Is(err, io.EOF) {
81
+		log.G(context.TODO()).Infof("encountered error during pull and clearing it before resume: %s", err)
82
+		err = nil
83
+	}
84
+	return n, err
85
+}
86
+
87
+func (r *requestReader) Close() error {
88
+	r.cleanUpResponse()
89
+	r.client = nil
90
+	r.request = nil
91
+	return nil
92
+}
93
+
94
+func (r *requestReader) cleanUpResponse() {
95
+	if r.currentResponse != nil {
96
+		r.currentResponse.Body.Close()
97
+		r.currentResponse = nil
98
+	}
99
+}
0 100
new file mode 100644
... ...
@@ -0,0 +1,258 @@
0
+package resumable
1
+
2
+import (
3
+	"errors"
4
+	"fmt"
5
+	"io"
6
+	"net/http"
7
+	"net/http/httptest"
8
+	"strings"
9
+	"testing"
10
+	"time"
11
+
12
+	"gotest.tools/v3/assert"
13
+	is "gotest.tools/v3/assert/cmp"
14
+)
15
+
16
+func TestResumableRequestHeaderSimpleErrors(t *testing.T) {
17
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18
+		fmt.Fprintln(w, "Hello, world !")
19
+	}))
20
+	defer ts.Close()
21
+
22
+	client := &http.Client{}
23
+
24
+	var req *http.Request
25
+	req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody)
26
+	assert.NilError(t, err)
27
+
28
+	resreq := &requestReader{}
29
+	_, err = resreq.Read([]byte{})
30
+	assert.Check(t, is.Error(err, "client and request can't be nil"))
31
+
32
+	resreq = &requestReader{
33
+		client:    client,
34
+		request:   req,
35
+		totalSize: -1,
36
+	}
37
+	_, err = resreq.Read([]byte{})
38
+	assert.Check(t, is.Error(err, "failed to auto detect content length"))
39
+}
40
+
41
+// Not too much failures, bails out after some wait
42
+func TestResumableRequestHeaderNotTooMuchFailures(t *testing.T) {
43
+	client := &http.Client{}
44
+
45
+	var badReq *http.Request
46
+	badReq, err := http.NewRequest(http.MethodGet, "I'm not an url", http.NoBody)
47
+	assert.NilError(t, err)
48
+
49
+	resreq := &requestReader{
50
+		client:       client,
51
+		request:      badReq,
52
+		failures:     0,
53
+		maxFailures:  2,
54
+		waitDuration: 10 * time.Millisecond,
55
+	}
56
+	read, err := resreq.Read([]byte{})
57
+	assert.NilError(t, err)
58
+	assert.Check(t, is.Equal(0, read))
59
+}
60
+
61
+// Too much failures, returns the error
62
+func TestResumableRequestHeaderTooMuchFailures(t *testing.T) {
63
+	client := &http.Client{}
64
+
65
+	var badReq *http.Request
66
+	badReq, err := http.NewRequest(http.MethodGet, "I'm not an url", http.NoBody)
67
+	assert.NilError(t, err)
68
+
69
+	resreq := &requestReader{
70
+		client:      client,
71
+		request:     badReq,
72
+		failures:    0,
73
+		maxFailures: 1,
74
+	}
75
+	defer resreq.Close()
76
+
77
+	read, err := resreq.Read([]byte{})
78
+	assert.Assert(t, err != nil)
79
+	assert.Check(t, is.ErrorContains(err, "unsupported protocol scheme"))
80
+	assert.Check(t, is.ErrorContains(err, "I%27m%20not%20an%20url"))
81
+	assert.Check(t, is.Equal(0, read))
82
+}
83
+
84
+type errorReaderCloser struct{}
85
+
86
+func (errorReaderCloser) Close() error { return nil }
87
+
88
+func (errorReaderCloser) Read(p []byte) (int, error) {
89
+	return 0, errors.New("an error occurred")
90
+}
91
+
92
+// If an unknown error is encountered, return 0, nil and log it
93
+func TestResumableRequestReaderWithReadError(t *testing.T) {
94
+	var req *http.Request
95
+	req, err := http.NewRequest(http.MethodGet, "", http.NoBody)
96
+	assert.NilError(t, err)
97
+
98
+	client := &http.Client{}
99
+
100
+	response := &http.Response{
101
+		Status:        "500 Internal Server",
102
+		StatusCode:    http.StatusInternalServerError,
103
+		ContentLength: 0,
104
+		Close:         true,
105
+		Body:          errorReaderCloser{},
106
+	}
107
+
108
+	resreq := &requestReader{
109
+		client:          client,
110
+		request:         req,
111
+		currentResponse: response,
112
+		lastRange:       1,
113
+		totalSize:       1,
114
+	}
115
+	defer resreq.Close()
116
+
117
+	buf := make([]byte, 1)
118
+	read, err := resreq.Read(buf)
119
+	assert.NilError(t, err)
120
+
121
+	assert.Check(t, is.Equal(0, read))
122
+}
123
+
124
+func TestResumableRequestReaderWithEOFWith416Response(t *testing.T) {
125
+	var req *http.Request
126
+	req, err := http.NewRequest(http.MethodGet, "", http.NoBody)
127
+	assert.NilError(t, err)
128
+
129
+	client := &http.Client{}
130
+
131
+	response := &http.Response{
132
+		Status:        "416 Requested Range Not Satisfiable",
133
+		StatusCode:    http.StatusRequestedRangeNotSatisfiable,
134
+		ContentLength: 0,
135
+		Close:         true,
136
+		Body:          io.NopCloser(strings.NewReader("")),
137
+	}
138
+
139
+	resreq := &requestReader{
140
+		client:          client,
141
+		request:         req,
142
+		currentResponse: response,
143
+		lastRange:       1,
144
+		totalSize:       1,
145
+	}
146
+	defer resreq.Close()
147
+
148
+	buf := make([]byte, 1)
149
+	_, err = resreq.Read(buf)
150
+	assert.Check(t, is.Error(err, io.EOF.Error()))
151
+}
152
+
153
+func TestResumableRequestReaderWithServerDoesntSupportByteRanges(t *testing.T) {
154
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
155
+		if r.Header.Get("Range") == "" {
156
+			t.Fatalf("Expected a Range HTTP header, got nothing")
157
+		}
158
+	}))
159
+	defer ts.Close()
160
+
161
+	var req *http.Request
162
+	req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody)
163
+	assert.NilError(t, err)
164
+
165
+	client := &http.Client{}
166
+
167
+	resreq := &requestReader{
168
+		client:    client,
169
+		request:   req,
170
+		lastRange: 1,
171
+	}
172
+	defer resreq.Close()
173
+
174
+	buf := make([]byte, 2)
175
+	_, err = resreq.Read(buf)
176
+	assert.Check(t, is.Error(err, "the server doesn't support byte ranges"))
177
+}
178
+
179
+func TestResumableRequestReaderWithZeroTotalSize(t *testing.T) {
180
+	srvtxt := "some response text data"
181
+
182
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
183
+		fmt.Fprintln(w, srvtxt)
184
+	}))
185
+	defer ts.Close()
186
+
187
+	var req *http.Request
188
+	req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody)
189
+	assert.NilError(t, err)
190
+
191
+	client := &http.Client{}
192
+	retries := uint32(5)
193
+
194
+	resreq := NewRequestReader(client, req, retries, 0)
195
+	defer resreq.Close()
196
+
197
+	data, err := io.ReadAll(resreq)
198
+	assert.NilError(t, err)
199
+
200
+	resstr := strings.TrimSuffix(string(data), "\n")
201
+	assert.Check(t, is.Equal(srvtxt, resstr))
202
+}
203
+
204
+func TestResumableRequestReader(t *testing.T) {
205
+	srvtxt := "some response text data"
206
+
207
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
208
+		fmt.Fprintln(w, srvtxt)
209
+	}))
210
+	defer ts.Close()
211
+
212
+	var req *http.Request
213
+	req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody)
214
+	assert.NilError(t, err)
215
+
216
+	client := &http.Client{}
217
+	retries := uint32(5)
218
+	imgSize := int64(len(srvtxt))
219
+
220
+	resreq := NewRequestReader(client, req, retries, imgSize)
221
+	defer resreq.Close()
222
+
223
+	data, err := io.ReadAll(resreq)
224
+	assert.NilError(t, err)
225
+
226
+	resstr := strings.TrimSuffix(string(data), "\n")
227
+	assert.Check(t, is.Equal(srvtxt, resstr))
228
+}
229
+
230
+func TestResumableRequestReaderWithInitialResponse(t *testing.T) {
231
+	srvtxt := "some response text data"
232
+
233
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
234
+		fmt.Fprintln(w, srvtxt)
235
+	}))
236
+	defer ts.Close()
237
+
238
+	var req *http.Request
239
+	req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody)
240
+	assert.NilError(t, err)
241
+
242
+	client := &http.Client{}
243
+	retries := uint32(5)
244
+	imgSize := int64(len(srvtxt))
245
+
246
+	res, err := client.Do(req)
247
+	assert.NilError(t, err)
248
+
249
+	resreq := NewRequestReaderWithInitialResponse(client, req, retries, imgSize, res)
250
+	defer resreq.Close()
251
+
252
+	data, err := io.ReadAll(resreq)
253
+	assert.NilError(t, err)
254
+
255
+	resstr := strings.TrimSuffix(string(data), "\n")
256
+	assert.Check(t, is.Equal(srvtxt, resstr))
257
+}
0 258
new file mode 100644
... ...
@@ -0,0 +1,170 @@
0
+package registry
1
+
2
+import (
3
+	"context"
4
+	"net/http"
5
+	"strconv"
6
+	"strings"
7
+
8
+	"github.com/containerd/log"
9
+	"github.com/docker/distribution/registry/client/auth"
10
+	"github.com/moby/moby/api/types/filters"
11
+	"github.com/moby/moby/api/types/registry"
12
+	"github.com/pkg/errors"
13
+)
14
+
15
+var acceptedSearchFilterTags = map[string]bool{
16
+	"is-automated": true, // Deprecated: the "is_automated" field is deprecated and will always be false in the future.
17
+	"is-official":  true,
18
+	"stars":        true,
19
+}
20
+
21
+// Search queries the public registry for repositories matching the specified
22
+// search term and filters.
23
+func (s *Service) Search(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *registry.AuthConfig, headers map[string][]string) ([]registry.SearchResult, error) {
24
+	if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil {
25
+		return nil, err
26
+	}
27
+
28
+	isAutomated, err := searchFilters.GetBoolOrDefault("is-automated", false)
29
+	if err != nil {
30
+		return nil, err
31
+	}
32
+
33
+	// "is-automated" is deprecated and filtering for `true` will yield no results.
34
+	if isAutomated {
35
+		return []registry.SearchResult{}, nil
36
+	}
37
+
38
+	isOfficial, err := searchFilters.GetBoolOrDefault("is-official", false)
39
+	if err != nil {
40
+		return nil, err
41
+	}
42
+
43
+	hasStarFilter := 0
44
+	if searchFilters.Contains("stars") {
45
+		hasStars := searchFilters.Get("stars")
46
+		for _, hasStar := range hasStars {
47
+			iHasStar, err := strconv.Atoi(hasStar)
48
+			if err != nil {
49
+				return nil, invalidParameterErr{errors.Wrapf(err, "invalid filter 'stars=%s'", hasStar)}
50
+			}
51
+			if iHasStar > hasStarFilter {
52
+				hasStarFilter = iHasStar
53
+			}
54
+		}
55
+	}
56
+
57
+	unfilteredResult, err := s.searchUnfiltered(ctx, term, limit, authConfig, headers)
58
+	if err != nil {
59
+		return nil, err
60
+	}
61
+
62
+	filteredResults := []registry.SearchResult{}
63
+	for _, result := range unfilteredResult.Results {
64
+		if searchFilters.Contains("is-official") {
65
+			if isOfficial != result.IsOfficial {
66
+				continue
67
+			}
68
+		}
69
+		if searchFilters.Contains("stars") {
70
+			if result.StarCount < hasStarFilter {
71
+				continue
72
+			}
73
+		}
74
+		// "is-automated" is deprecated and the value in Docker Hub search
75
+		// results is untrustworthy. Force it to false so as to not mislead our
76
+		// clients.
77
+		result.IsAutomated = false //nolint:staticcheck  // ignore SA1019 (field is deprecated)
78
+		filteredResults = append(filteredResults, result)
79
+	}
80
+
81
+	return filteredResults, nil
82
+}
83
+
84
+func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int, authConfig *registry.AuthConfig, headers http.Header) (*registry.SearchResults, error) {
85
+	if hasScheme(term) {
86
+		return nil, invalidParamf("invalid repository name: repository name (%s) should not have a scheme", term)
87
+	}
88
+
89
+	indexName, remoteName := splitReposSearchTerm(term)
90
+
91
+	// Search is a long-running operation, just lock s.config to avoid block others.
92
+	s.mu.RLock()
93
+	index := newIndexInfo(s.config, indexName)
94
+	s.mu.RUnlock()
95
+	if index.Official {
96
+		// If pull "library/foo", it's stored locally under "foo"
97
+		remoteName = strings.TrimPrefix(remoteName, "library/")
98
+	}
99
+
100
+	endpoint, err := newV1Endpoint(ctx, index, headers)
101
+	if err != nil {
102
+		return nil, err
103
+	}
104
+
105
+	var client *http.Client
106
+	if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" {
107
+		creds := NewStaticCredentialStore(authConfig)
108
+
109
+		// TODO(thaJeztah); is there a reason not to include other headers here? (originally added in 19d48f0b8ba59eea9f2cac4ad1c7977712a6b7ac)
110
+		modifiers := Headers(headers.Get("User-Agent"), nil)
111
+		v2Client, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, []auth.Scope{
112
+			auth.RegistryScope{Name: "catalog", Actions: []string{"search"}},
113
+		})
114
+		if err != nil {
115
+			return nil, err
116
+		}
117
+		// Copy non transport http client features
118
+		v2Client.Timeout = endpoint.client.Timeout
119
+		v2Client.CheckRedirect = endpoint.client.CheckRedirect
120
+		v2Client.Jar = endpoint.client.Jar
121
+
122
+		log.G(ctx).Debugf("using v2 client for search to %s", endpoint.URL)
123
+		client = v2Client
124
+	} else {
125
+		client = endpoint.client
126
+		if err := authorizeClient(ctx, client, authConfig, endpoint); err != nil {
127
+			return nil, err
128
+		}
129
+	}
130
+
131
+	return newSession(client, endpoint).searchRepositories(ctx, remoteName, limit)
132
+}
133
+
134
+// splitReposSearchTerm breaks a search term into an index name and remote name
135
+func splitReposSearchTerm(reposName string) (string, string) {
136
+	nameParts := strings.SplitN(reposName, "/", 2)
137
+	if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") &&
138
+		!strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") {
139
+		// This is a Docker Hub repository (ex: samalba/hipache or ubuntu),
140
+		// use the default Docker Hub registry (docker.io)
141
+		return IndexName, reposName
142
+	}
143
+	return nameParts[0], nameParts[1]
144
+}
145
+
146
+// ParseSearchIndexInfo will use repository name to get back an indexInfo.
147
+//
148
+// TODO(thaJeztah) this function is only used by the CLI, and used to get
149
+// information of the registry (to provide credentials if needed). We should
150
+// move this function (or equivalent) to the CLI, as it's doing too much just
151
+// for that.
152
+func ParseSearchIndexInfo(reposName string) (*registry.IndexInfo, error) {
153
+	indexName, _ := splitReposSearchTerm(reposName)
154
+	indexName = normalizeIndexName(indexName)
155
+	if indexName == IndexName {
156
+		return &registry.IndexInfo{
157
+			Name:     IndexName,
158
+			Mirrors:  []string{},
159
+			Secure:   true,
160
+			Official: true,
161
+		}, nil
162
+	}
163
+
164
+	return &registry.IndexInfo{
165
+		Name:    indexName,
166
+		Mirrors: []string{},
167
+		Secure:  !isInsecure(indexName),
168
+	}, nil
169
+}
0 170
new file mode 100644
... ...
@@ -0,0 +1,213 @@
0
+package registry
1
+
2
+import (
3
+	"context"
4
+	"crypto/tls"
5
+	"encoding/json"
6
+	"errors"
7
+	"fmt"
8
+	"net/http"
9
+	"net/url"
10
+	"strings"
11
+
12
+	"github.com/containerd/log"
13
+	"github.com/docker/distribution/registry/client/transport"
14
+	"github.com/moby/moby/api/types/registry"
15
+)
16
+
17
+// v1PingResult contains the information returned when pinging a registry. It
18
+// indicates whether the registry claims to be a standalone registry.
19
+type v1PingResult struct {
20
+	// Standalone is set to true if the registry indicates it is a
21
+	// standalone registry in the X-Docker-Registry-Standalone
22
+	// header
23
+	Standalone bool `json:"standalone"`
24
+}
25
+
26
+// v1Endpoint stores basic information about a V1 registry endpoint.
27
+type v1Endpoint struct {
28
+	client   *http.Client
29
+	URL      *url.URL
30
+	IsSecure bool
31
+}
32
+
33
+// newV1Endpoint parses the given address to return a registry endpoint.
34
+// TODO: remove. This is only used by search.
35
+func newV1Endpoint(ctx context.Context, index *registry.IndexInfo, headers http.Header) (*v1Endpoint, error) {
36
+	tlsConfig, err := newTLSConfig(ctx, index.Name, index.Secure)
37
+	if err != nil {
38
+		return nil, err
39
+	}
40
+
41
+	endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, headers)
42
+	if err != nil {
43
+		return nil, err
44
+	}
45
+
46
+	if endpoint.String() == IndexServer {
47
+		// Skip the check, we know this one is valid
48
+		// (and we never want to fall back to http in case of error)
49
+		return endpoint, nil
50
+	}
51
+
52
+	// Try HTTPS ping to registry
53
+	endpoint.URL.Scheme = "https"
54
+	if _, err := endpoint.ping(ctx); err != nil {
55
+		if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
56
+			return nil, err
57
+		}
58
+		if endpoint.IsSecure {
59
+			// If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry`
60
+			// in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fall back to HTTP.
61
+			hint := fmt.Sprintf(
62
+				". If this private registry supports only HTTP or HTTPS with an unknown CA certificate, add `--insecure-registry %[1]s` to the daemon's arguments. "+
63
+					"In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; place the CA certificate at /etc/docker/certs.d/%[1]s/ca.crt",
64
+				endpoint.URL.Host,
65
+			)
66
+			return nil, invalidParamf("invalid registry endpoint %s: %v%s", endpoint, err, hint)
67
+		}
68
+
69
+		// registry is insecure and HTTPS failed, fallback to HTTP.
70
+		log.G(ctx).WithError(err).Debugf("error from registry %q marked as insecure - insecurely falling back to HTTP", endpoint)
71
+		endpoint.URL.Scheme = "http"
72
+		if _, err2 := endpoint.ping(ctx); err2 != nil {
73
+			return nil, invalidParamf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
74
+		}
75
+	}
76
+
77
+	return endpoint, nil
78
+}
79
+
80
+// trimV1Address trims the "v1" version suffix off the address and returns
81
+// the trimmed address. It returns an error on "v2" endpoints.
82
+func trimV1Address(address string) (string, error) {
83
+	trimmed := strings.TrimSuffix(address, "/")
84
+	if strings.HasSuffix(trimmed, "/v2") {
85
+		return "", invalidParamf("search is not supported on v2 endpoints: %s", address)
86
+	}
87
+	return strings.TrimSuffix(trimmed, "/v1"), nil
88
+}
89
+
90
+func newV1EndpointFromStr(address string, tlsConfig *tls.Config, headers http.Header) (*v1Endpoint, error) {
91
+	if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
92
+		address = "https://" + address
93
+	}
94
+
95
+	address, err := trimV1Address(address)
96
+	if err != nil {
97
+		return nil, err
98
+	}
99
+
100
+	uri, err := url.Parse(address)
101
+	if err != nil {
102
+		return nil, invalidParam(err)
103
+	}
104
+
105
+	// TODO(tiborvass): make sure a ConnectTimeout transport is used
106
+	tr := newTransport(tlsConfig)
107
+
108
+	return &v1Endpoint{
109
+		IsSecure: tlsConfig == nil || !tlsConfig.InsecureSkipVerify,
110
+		URL:      uri,
111
+		client:   httpClient(transport.NewTransport(tr, Headers("", headers)...)),
112
+	}, nil
113
+}
114
+
115
+// Get the formatted URL for the root of this registry Endpoint
116
+func (e *v1Endpoint) String() string {
117
+	return e.URL.String() + "/v1/"
118
+}
119
+
120
+// ping returns a v1PingResult which indicates whether the registry is standalone or not.
121
+func (e *v1Endpoint) ping(ctx context.Context) (v1PingResult, error) {
122
+	if e.String() == IndexServer {
123
+		// Skip the check, we know this one is valid
124
+		// (and we never want to fallback to http in case of error)
125
+		return v1PingResult{}, nil
126
+	}
127
+
128
+	pingURL := e.String() + "_ping"
129
+	log.G(ctx).WithField("url", pingURL).Debug("attempting v1 ping for registry endpoint")
130
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, pingURL, http.NoBody)
131
+	if err != nil {
132
+		return v1PingResult{}, invalidParam(err)
133
+	}
134
+
135
+	resp, err := e.client.Do(req)
136
+	if err != nil {
137
+		if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
138
+			return v1PingResult{}, err
139
+		}
140
+		return v1PingResult{}, invalidParam(err)
141
+	}
142
+
143
+	defer resp.Body.Close()
144
+
145
+	if v := resp.Header.Get("X-Docker-Registry-Standalone"); v != "" {
146
+		info := v1PingResult{}
147
+		// Accepted values are "1", and "true" (case-insensitive).
148
+		if v == "1" || strings.EqualFold(v, "true") {
149
+			info.Standalone = true
150
+		}
151
+		log.G(ctx).Debugf("v1PingResult.Standalone (from X-Docker-Registry-Standalone header): %t", info.Standalone)
152
+		return info, nil
153
+	}
154
+
155
+	// If the header is absent, we assume true for compatibility with earlier
156
+	// versions of the registry. default to true
157
+	info := v1PingResult{
158
+		Standalone: true,
159
+	}
160
+	if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
161
+		log.G(ctx).WithError(err).Debug("error unmarshaling _ping response")
162
+		// don't stop here. Just assume sane defaults
163
+	}
164
+
165
+	log.G(ctx).Debugf("v1PingResult.Standalone: %t", info.Standalone)
166
+	return info, nil
167
+}
168
+
169
+// httpClient returns an HTTP client structure which uses the given transport
170
+// and contains the necessary headers for redirected requests
171
+func httpClient(tr http.RoundTripper) *http.Client {
172
+	return &http.Client{
173
+		Transport:     tr,
174
+		CheckRedirect: addRequiredHeadersToRedirectedRequests,
175
+	}
176
+}
177
+
178
+func trustedLocation(req *http.Request) bool {
179
+	var (
180
+		trusteds = []string{"docker.com", "docker.io"}
181
+		hostname = strings.SplitN(req.Host, ":", 2)[0]
182
+	)
183
+	if req.URL.Scheme != "https" {
184
+		return false
185
+	}
186
+
187
+	for _, trusted := range trusteds {
188
+		if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) {
189
+			return true
190
+		}
191
+	}
192
+	return false
193
+}
194
+
195
+// addRequiredHeadersToRedirectedRequests adds the necessary redirection headers
196
+// for redirected requests
197
+func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error {
198
+	if len(via) != 0 && via[0] != nil {
199
+		if trustedLocation(req) && trustedLocation(via[0]) {
200
+			req.Header = via[0].Header
201
+			return nil
202
+		}
203
+		for k, v := range via[0].Header {
204
+			if k != "Authorization" {
205
+				for _, vv := range v {
206
+					req.Header.Add(k, vv)
207
+				}
208
+			}
209
+		}
210
+	}
211
+	return nil
212
+}
0 213
new file mode 100644
... ...
@@ -0,0 +1,237 @@
0
+package registry
1
+
2
+import (
3
+	"context"
4
+	"net/http"
5
+	"net/http/httptest"
6
+	"strings"
7
+	"testing"
8
+
9
+	"github.com/moby/moby/api/types/registry"
10
+	"gotest.tools/v3/assert"
11
+	is "gotest.tools/v3/assert/cmp"
12
+)
13
+
14
+func TestV1EndpointPing(t *testing.T) {
15
+	testPing := func(index *registry.IndexInfo, expectedStandalone bool, assertMessage string) {
16
+		ep, err := newV1Endpoint(context.Background(), index, nil)
17
+		if err != nil {
18
+			t.Fatal(err)
19
+		}
20
+		regInfo, err := ep.ping(context.Background())
21
+		if err != nil {
22
+			t.Fatal(err)
23
+		}
24
+
25
+		assert.Equal(t, regInfo.Standalone, expectedStandalone, assertMessage)
26
+	}
27
+
28
+	testPing(makeIndex("/v1/"), true, "Expected standalone to be true (default)")
29
+	testPing(makeHTTPSIndex("/v1/"), true, "Expected standalone to be true (default)")
30
+	testPing(makePublicIndex(), false, "Expected standalone to be false for public index")
31
+}
32
+
33
+func TestV1Endpoint(t *testing.T) {
34
+	// Simple wrapper to fail test if err != nil
35
+	expandEndpoint := func(index *registry.IndexInfo) *v1Endpoint {
36
+		endpoint, err := newV1Endpoint(context.Background(), index, nil)
37
+		if err != nil {
38
+			t.Fatal(err)
39
+		}
40
+		return endpoint
41
+	}
42
+
43
+	assertInsecureIndex := func(index *registry.IndexInfo) {
44
+		index.Secure = true
45
+		_, err := newV1Endpoint(context.Background(), index, nil)
46
+		assert.ErrorContains(t, err, "insecure-registry", index.Name+": Expected insecure-registry  error for insecure index")
47
+		index.Secure = false
48
+	}
49
+
50
+	assertSecureIndex := func(index *registry.IndexInfo) {
51
+		index.Secure = true
52
+		_, err := newV1Endpoint(context.Background(), index, nil)
53
+		assert.ErrorContains(t, err, "certificate signed by unknown authority", index.Name+": Expected cert error for secure index")
54
+		index.Secure = false
55
+	}
56
+
57
+	index := &registry.IndexInfo{}
58
+	index.Name = makeURL("/v1/")
59
+	endpoint := expandEndpoint(index)
60
+	assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
61
+	assertInsecureIndex(index)
62
+
63
+	index.Name = makeURL("")
64
+	endpoint = expandEndpoint(index)
65
+	assert.Equal(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
66
+	assertInsecureIndex(index)
67
+
68
+	httpURL := makeURL("")
69
+	index.Name = strings.SplitN(httpURL, "://", 2)[1]
70
+	endpoint = expandEndpoint(index)
71
+	assert.Equal(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/")
72
+	assertInsecureIndex(index)
73
+
74
+	index.Name = makeHTTPSURL("/v1/")
75
+	endpoint = expandEndpoint(index)
76
+	assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
77
+	assertSecureIndex(index)
78
+
79
+	index.Name = makeHTTPSURL("")
80
+	endpoint = expandEndpoint(index)
81
+	assert.Equal(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
82
+	assertSecureIndex(index)
83
+
84
+	httpsURL := makeHTTPSURL("")
85
+	index.Name = strings.SplitN(httpsURL, "://", 2)[1]
86
+	endpoint = expandEndpoint(index)
87
+	assert.Equal(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/")
88
+	assertSecureIndex(index)
89
+
90
+	badEndpoints := []string{
91
+		"http://127.0.0.1/v1/",
92
+		"https://127.0.0.1/v1/",
93
+		"http://127.0.0.1",
94
+		"https://127.0.0.1",
95
+		"127.0.0.1",
96
+	}
97
+	for _, address := range badEndpoints {
98
+		index.Name = address
99
+		_, err := newV1Endpoint(context.Background(), index, nil)
100
+		assert.Check(t, err != nil, "Expected error while expanding bad endpoint: %s", address)
101
+	}
102
+}
103
+
104
+func TestV1EndpointParse(t *testing.T) {
105
+	tests := []struct {
106
+		address     string
107
+		expected    string
108
+		expectedErr string
109
+	}{
110
+		{
111
+			address:  IndexServer,
112
+			expected: IndexServer,
113
+		},
114
+		{
115
+			address:  "https://0.0.0.0:5000/v1/",
116
+			expected: "https://0.0.0.0:5000/v1/",
117
+		},
118
+		{
119
+			address:  "https://0.0.0.0:5000",
120
+			expected: "https://0.0.0.0:5000/v1/",
121
+		},
122
+		{
123
+			address:  "0.0.0.0:5000",
124
+			expected: "https://0.0.0.0:5000/v1/",
125
+		},
126
+		{
127
+			address:  "https://0.0.0.0:5000/nonversion/",
128
+			expected: "https://0.0.0.0:5000/nonversion/v1/",
129
+		},
130
+		{
131
+			address:  "https://0.0.0.0:5000/v0/",
132
+			expected: "https://0.0.0.0:5000/v0/v1/",
133
+		},
134
+		{
135
+			address:     "https://0.0.0.0:5000/v2/",
136
+			expectedErr: "search is not supported on v2 endpoints: https://0.0.0.0:5000/v2/",
137
+		},
138
+	}
139
+	for _, tc := range tests {
140
+		t.Run(tc.address, func(t *testing.T) {
141
+			ep, err := newV1EndpointFromStr(tc.address, nil, nil)
142
+			if tc.expectedErr != "" {
143
+				assert.Check(t, is.Error(err, tc.expectedErr))
144
+				assert.Check(t, is.Nil(ep))
145
+			} else {
146
+				assert.NilError(t, err)
147
+				assert.Check(t, is.Equal(ep.String(), tc.expected))
148
+			}
149
+		})
150
+	}
151
+}
152
+
153
+// Ensure that a registry endpoint that responds with a 401 only is determined
154
+// to be a valid v1 registry endpoint
155
+func TestV1EndpointValidate(t *testing.T) {
156
+	requireBasicAuthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
157
+		w.Header().Add("WWW-Authenticate", `Basic realm="localhost"`)
158
+		w.WriteHeader(http.StatusUnauthorized)
159
+	})
160
+
161
+	// Make a test server which should validate as a v1 server.
162
+	testServer := httptest.NewServer(requireBasicAuthHandler)
163
+	defer testServer.Close()
164
+
165
+	testEndpoint, err := newV1Endpoint(context.Background(), &registry.IndexInfo{Name: testServer.URL}, nil)
166
+	if err != nil {
167
+		t.Fatal(err)
168
+	}
169
+
170
+	if testEndpoint.URL.Scheme != "http" {
171
+		t.Fatalf("expecting to validate endpoint as http, got url %s", testEndpoint.String())
172
+	}
173
+}
174
+
175
+func TestTrustedLocation(t *testing.T) {
176
+	for _, u := range []string{"http://example.com", "https://example.com:7777", "http://docker.io", "http://test.docker.com", "https://fakedocker.com"} {
177
+		req, _ := http.NewRequest(http.MethodGet, u, http.NoBody)
178
+		assert.Check(t, !trustedLocation(req))
179
+	}
180
+
181
+	for _, u := range []string{"https://docker.io", "https://test.docker.com:80"} {
182
+		req, _ := http.NewRequest(http.MethodGet, u, http.NoBody)
183
+		assert.Check(t, trustedLocation(req))
184
+	}
185
+}
186
+
187
+func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) {
188
+	for _, urls := range [][]string{
189
+		{"http://docker.io", "https://docker.com"},
190
+		{"https://foo.docker.io:7777", "http://bar.docker.com"},
191
+		{"https://foo.docker.io", "https://example.com"},
192
+	} {
193
+		reqFrom, _ := http.NewRequest(http.MethodGet, urls[0], http.NoBody)
194
+		reqFrom.Header.Add("Content-Type", "application/json")
195
+		reqFrom.Header.Add("Authorization", "super_secret")
196
+		reqTo, _ := http.NewRequest(http.MethodGet, urls[1], http.NoBody)
197
+
198
+		_ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
199
+
200
+		if len(reqTo.Header) != 1 {
201
+			t.Fatalf("Expected 1 headers, got %d", len(reqTo.Header))
202
+		}
203
+
204
+		if reqTo.Header.Get("Content-Type") != "application/json" {
205
+			t.Fatal("'Content-Type' should be 'application/json'")
206
+		}
207
+
208
+		if reqTo.Header.Get("Authorization") != "" {
209
+			t.Fatal("'Authorization' should be empty")
210
+		}
211
+	}
212
+
213
+	for _, urls := range [][]string{
214
+		{"https://docker.io", "https://docker.com"},
215
+		{"https://foo.docker.io:7777", "https://bar.docker.com"},
216
+	} {
217
+		reqFrom, _ := http.NewRequest(http.MethodGet, urls[0], http.NoBody)
218
+		reqFrom.Header.Add("Content-Type", "application/json")
219
+		reqFrom.Header.Add("Authorization", "super_secret")
220
+		reqTo, _ := http.NewRequest(http.MethodGet, urls[1], http.NoBody)
221
+
222
+		_ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
223
+
224
+		if len(reqTo.Header) != 2 {
225
+			t.Fatalf("Expected 2 headers, got %d", len(reqTo.Header))
226
+		}
227
+
228
+		if reqTo.Header.Get("Content-Type") != "application/json" {
229
+			t.Fatal("'Content-Type' should be 'application/json'")
230
+		}
231
+
232
+		if reqTo.Header.Get("Authorization") != "super_secret" {
233
+			t.Fatal("'Authorization' should be 'super_secret'")
234
+		}
235
+	}
236
+}
0 237
new file mode 100644
... ...
@@ -0,0 +1,248 @@
0
+package registry
1
+
2
+import (
3
+	// this is required for some certificates
4
+	"context"
5
+	_ "crypto/sha512"
6
+	"encoding/json"
7
+	"fmt"
8
+	"io"
9
+	"net/http"
10
+	"net/http/cookiejar"
11
+	"net/url"
12
+	"strconv"
13
+	"strings"
14
+	"sync"
15
+
16
+	"github.com/containerd/log"
17
+	"github.com/moby/moby/api/types/registry"
18
+	"github.com/pkg/errors"
19
+)
20
+
21
+// A session is used to communicate with a V1 registry
22
+type session struct {
23
+	indexEndpoint *v1Endpoint
24
+	client        *http.Client
25
+}
26
+
27
+type authTransport struct {
28
+	base       http.RoundTripper
29
+	authConfig *registry.AuthConfig
30
+
31
+	alwaysSetBasicAuth bool
32
+	token              []string
33
+
34
+	mu     sync.Mutex                      // guards modReq
35
+	modReq map[*http.Request]*http.Request // original -> modified
36
+}
37
+
38
+// newAuthTransport handles the auth layer when communicating with a v1 registry (private or official)
39
+//
40
+// For private v1 registries, set alwaysSetBasicAuth to true.
41
+//
42
+// For the official v1 registry, if there isn't already an Authorization header in the request,
43
+// but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header.
44
+// After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing
45
+// a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent
46
+// requests.
47
+//
48
+// If the server sends a token without the client having requested it, it is ignored.
49
+//
50
+// This RoundTripper also has a CancelRequest method important for correct timeout handling.
51
+func newAuthTransport(base http.RoundTripper, authConfig *registry.AuthConfig, alwaysSetBasicAuth bool) *authTransport {
52
+	if base == nil {
53
+		base = http.DefaultTransport
54
+	}
55
+	return &authTransport{
56
+		base:               base,
57
+		authConfig:         authConfig,
58
+		alwaysSetBasicAuth: alwaysSetBasicAuth,
59
+		modReq:             make(map[*http.Request]*http.Request),
60
+	}
61
+}
62
+
63
+// cloneRequest returns a clone of the provided *http.Request.
64
+// The clone is a shallow copy of the struct and its Header map.
65
+func cloneRequest(r *http.Request) *http.Request {
66
+	// shallow copy of the struct
67
+	r2 := new(http.Request)
68
+	*r2 = *r
69
+	// deep copy of the Header
70
+	r2.Header = make(http.Header, len(r.Header))
71
+	for k, s := range r.Header {
72
+		r2.Header[k] = append([]string(nil), s...)
73
+	}
74
+
75
+	return r2
76
+}
77
+
78
+// onEOFReader wraps an io.ReadCloser and a function
79
+// the function will run at the end of file or close the file.
80
+type onEOFReader struct {
81
+	Rc io.ReadCloser
82
+	Fn func()
83
+}
84
+
85
+func (r *onEOFReader) Read(p []byte) (int, error) {
86
+	n, err := r.Rc.Read(p)
87
+	if err == io.EOF {
88
+		r.runFunc()
89
+	}
90
+	return n, err
91
+}
92
+
93
+// Close closes the file and run the function.
94
+func (r *onEOFReader) Close() error {
95
+	err := r.Rc.Close()
96
+	r.runFunc()
97
+	return err
98
+}
99
+
100
+func (r *onEOFReader) runFunc() {
101
+	if fn := r.Fn; fn != nil {
102
+		fn()
103
+		r.Fn = nil
104
+	}
105
+}
106
+
107
+// RoundTrip changes an HTTP request's headers to add the necessary
108
+// authentication-related headers
109
+func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) {
110
+	// Authorization should not be set on 302 redirect for untrusted locations.
111
+	// This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests.
112
+	// As the authorization logic is currently implemented in RoundTrip,
113
+	// a 302 redirect is detected by looking at the Referrer header as go http package adds said header.
114
+	// This is safe as Docker doesn't set Referrer in other scenarios.
115
+	if orig.Header.Get("Referer") != "" && !trustedLocation(orig) {
116
+		return tr.base.RoundTrip(orig)
117
+	}
118
+
119
+	req := cloneRequest(orig)
120
+	tr.mu.Lock()
121
+	tr.modReq[orig] = req
122
+	tr.mu.Unlock()
123
+
124
+	if tr.alwaysSetBasicAuth {
125
+		if tr.authConfig == nil {
126
+			return nil, errors.New("unexpected error: empty auth config")
127
+		}
128
+		req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password)
129
+		return tr.base.RoundTrip(req)
130
+	}
131
+
132
+	// Don't override
133
+	if req.Header.Get("Authorization") == "" {
134
+		if req.Header.Get("X-Docker-Token") == "true" && tr.authConfig != nil && tr.authConfig.Username != "" {
135
+			req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password)
136
+		} else if len(tr.token) > 0 {
137
+			req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ","))
138
+		}
139
+	}
140
+	resp, err := tr.base.RoundTrip(req)
141
+	if err != nil {
142
+		tr.mu.Lock()
143
+		delete(tr.modReq, orig)
144
+		tr.mu.Unlock()
145
+		return nil, err
146
+	}
147
+	if len(resp.Header["X-Docker-Token"]) > 0 {
148
+		tr.token = resp.Header["X-Docker-Token"]
149
+	}
150
+	resp.Body = &onEOFReader{
151
+		Rc: resp.Body,
152
+		Fn: func() {
153
+			tr.mu.Lock()
154
+			delete(tr.modReq, orig)
155
+			tr.mu.Unlock()
156
+		},
157
+	}
158
+	return resp, nil
159
+}
160
+
161
+// CancelRequest cancels an in-flight request by closing its connection.
162
+func (tr *authTransport) CancelRequest(req *http.Request) {
163
+	type canceler interface {
164
+		CancelRequest(*http.Request)
165
+	}
166
+	if cr, ok := tr.base.(canceler); ok {
167
+		tr.mu.Lock()
168
+		modReq := tr.modReq[req]
169
+		delete(tr.modReq, req)
170
+		tr.mu.Unlock()
171
+		cr.CancelRequest(modReq)
172
+	}
173
+}
174
+
175
+func authorizeClient(ctx context.Context, client *http.Client, authConfig *registry.AuthConfig, endpoint *v1Endpoint) error {
176
+	var alwaysSetBasicAuth bool
177
+
178
+	// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
179
+	// alongside all our requests.
180
+	if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" {
181
+		info, err := endpoint.ping(ctx)
182
+		if err != nil {
183
+			return err
184
+		}
185
+		if info.Standalone && authConfig != nil {
186
+			log.G(ctx).WithField("endpoint", endpoint.String()).Debug("Endpoint is eligible for private registry; enabling alwaysSetBasicAuth")
187
+			alwaysSetBasicAuth = true
188
+		}
189
+	}
190
+
191
+	// Annotate the transport unconditionally so that v2 can
192
+	// properly fallback on v1 when an image is not found.
193
+	client.Transport = newAuthTransport(client.Transport, authConfig, alwaysSetBasicAuth)
194
+
195
+	jar, err := cookiejar.New(nil)
196
+	if err != nil {
197
+		return systemErr{errors.New("cookiejar.New is not supposed to return an error")}
198
+	}
199
+	client.Jar = jar
200
+
201
+	return nil
202
+}
203
+
204
+func newSession(client *http.Client, endpoint *v1Endpoint) *session {
205
+	return &session{
206
+		client:        client,
207
+		indexEndpoint: endpoint,
208
+	}
209
+}
210
+
211
+// defaultSearchLimit is the default value for maximum number of returned search results.
212
+const defaultSearchLimit = 25
213
+
214
+// searchRepositories performs a search against the remote repository
215
+func (r *session) searchRepositories(ctx context.Context, term string, limit int) (*registry.SearchResults, error) {
216
+	if limit == 0 {
217
+		limit = defaultSearchLimit
218
+	}
219
+	if limit < 1 || limit > 100 {
220
+		return nil, invalidParamf("limit %d is outside the range of [1, 100]", limit)
221
+	}
222
+	u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(strconv.Itoa(limit))
223
+	log.G(ctx).WithField("url", u).Debug("searchRepositories")
224
+
225
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, http.NoBody)
226
+	if err != nil {
227
+		return nil, invalidParamWrapf(err, "error building request")
228
+	}
229
+	// Have the AuthTransport send authentication, when logged in.
230
+	req.Header.Set("X-Docker-Token", "true")
231
+	res, err := r.client.Do(req)
232
+	if err != nil {
233
+		return nil, systemErr{err}
234
+	}
235
+	defer res.Body.Close()
236
+	if res.StatusCode != http.StatusOK {
237
+		// TODO(thaJeztah): return upstream response body for errors (see https://github.com/moby/moby/issues/27286).
238
+		// TODO(thaJeztah): handle other status-codes to return correct error-type
239
+		return nil, errUnknown{fmt.Errorf("unexpected status code %d", res.StatusCode)}
240
+	}
241
+	result := &registry.SearchResults{}
242
+	err = json.NewDecoder(res.Body).Decode(result)
243
+	if err != nil {
244
+		return nil, systemErr{errors.Wrap(err, "error decoding registry search results")}
245
+	}
246
+	return result, nil
247
+}
0 248
new file mode 100644
... ...
@@ -0,0 +1,418 @@
0
+package registry
1
+
2
+import (
3
+	"context"
4
+	"encoding/json"
5
+	"net/http"
6
+	"net/http/httptest"
7
+	"net/http/httputil"
8
+	"testing"
9
+
10
+	cerrdefs "github.com/containerd/errdefs"
11
+	"github.com/docker/distribution/registry/client/transport"
12
+	"github.com/moby/moby/api/types/filters"
13
+	"github.com/moby/moby/api/types/registry"
14
+	"gotest.tools/v3/assert"
15
+)
16
+
17
+func spawnTestRegistrySession(t *testing.T) *session {
18
+	t.Helper()
19
+	authConfig := &registry.AuthConfig{}
20
+	endpoint, err := newV1Endpoint(context.Background(), makeIndex("/v1/"), nil)
21
+	if err != nil {
22
+		t.Fatal(err)
23
+	}
24
+	userAgent := "docker test client"
25
+	var tr http.RoundTripper = debugTransport{newTransport(nil), t.Log}
26
+	tr = transport.NewTransport(newAuthTransport(tr, authConfig, false), Headers(userAgent, nil)...)
27
+	client := httpClient(tr)
28
+
29
+	if err := authorizeClient(context.Background(), client, authConfig, endpoint); err != nil {
30
+		t.Fatal(err)
31
+	}
32
+	r := newSession(client, endpoint)
33
+
34
+	// In a normal scenario for the v1 registry, the client should send a `X-Docker-Token: true`
35
+	// header while authenticating, in order to retrieve a token that can be later used to
36
+	// perform authenticated actions.
37
+	//
38
+	// The mock v1 registry does not support that, (TODO(tiborvass): support it), instead,
39
+	// it will consider authenticated any request with the header `X-Docker-Token: fake-token`.
40
+	//
41
+	// Because we know that the client's transport is an `*authTransport` we simply cast it,
42
+	// in order to set the internal cached token to the fake token, and thus send that fake token
43
+	// upon every subsequent requests.
44
+	r.client.Transport.(*authTransport).token = []string{"fake-token"}
45
+	return r
46
+}
47
+
48
+type debugTransport struct {
49
+	http.RoundTripper
50
+	log func(...interface{})
51
+}
52
+
53
+func (tr debugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
54
+	dump, err := httputil.DumpRequestOut(req, false)
55
+	if err != nil {
56
+		tr.log("could not dump request")
57
+	}
58
+	tr.log(string(dump))
59
+	resp, err := tr.RoundTripper.RoundTrip(req)
60
+	if err != nil {
61
+		return nil, err
62
+	}
63
+	dump, err = httputil.DumpResponse(resp, false)
64
+	if err != nil {
65
+		tr.log("could not dump response")
66
+	}
67
+	tr.log(string(dump))
68
+	return resp, err
69
+}
70
+
71
+func TestSearchRepositories(t *testing.T) {
72
+	r := spawnTestRegistrySession(t)
73
+	results, err := r.searchRepositories(context.Background(), "fakequery", 25)
74
+	if err != nil {
75
+		t.Fatal(err)
76
+	}
77
+	if results == nil {
78
+		t.Fatal("Expected non-nil SearchResults object")
79
+	}
80
+	assert.Equal(t, results.NumResults, 1, "Expected 1 search results")
81
+	assert.Equal(t, results.Query, "fakequery", "Expected 'fakequery' as query")
82
+	assert.Equal(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' to have 42 stars")
83
+}
84
+
85
+func TestSearchErrors(t *testing.T) {
86
+	errorCases := []struct {
87
+		filtersArgs       filters.Args
88
+		shouldReturnError bool
89
+		expectedError     string
90
+	}{
91
+		{
92
+			expectedError:     "unexpected status code 500",
93
+			shouldReturnError: true,
94
+		},
95
+		{
96
+			filtersArgs:   filters.NewArgs(filters.Arg("type", "custom")),
97
+			expectedError: "invalid filter 'type'",
98
+		},
99
+		{
100
+			filtersArgs:   filters.NewArgs(filters.Arg("is-automated", "invalid")),
101
+			expectedError: "invalid filter 'is-automated=[invalid]'",
102
+		},
103
+		{
104
+			filtersArgs: filters.NewArgs(
105
+				filters.Arg("is-automated", "true"),
106
+				filters.Arg("is-automated", "false"),
107
+			),
108
+			expectedError: "invalid filter 'is-automated",
109
+		},
110
+		{
111
+			filtersArgs:   filters.NewArgs(filters.Arg("is-official", "invalid")),
112
+			expectedError: "invalid filter 'is-official=[invalid]'",
113
+		},
114
+		{
115
+			filtersArgs: filters.NewArgs(
116
+				filters.Arg("is-official", "true"),
117
+				filters.Arg("is-official", "false"),
118
+			),
119
+			expectedError: "invalid filter 'is-official",
120
+		},
121
+		{
122
+			filtersArgs:   filters.NewArgs(filters.Arg("stars", "invalid")),
123
+			expectedError: "invalid filter 'stars=invalid'",
124
+		},
125
+		{
126
+			filtersArgs: filters.NewArgs(
127
+				filters.Arg("stars", "1"),
128
+				filters.Arg("stars", "invalid"),
129
+			),
130
+			expectedError: "invalid filter 'stars=invalid'",
131
+		},
132
+	}
133
+	for _, tc := range errorCases {
134
+		t.Run(tc.expectedError, func(t *testing.T) {
135
+			srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
136
+				if !tc.shouldReturnError {
137
+					t.Errorf("unexpected HTTP request")
138
+				}
139
+				http.Error(w, "no search for you", http.StatusInternalServerError)
140
+			}))
141
+			defer srv.Close()
142
+
143
+			// Construct the search term by cutting the 'http://' prefix off srv.URL.
144
+			term := srv.URL[7:] + "/term"
145
+
146
+			reg, err := NewService(ServiceOptions{})
147
+			assert.NilError(t, err)
148
+			_, err = reg.Search(context.Background(), tc.filtersArgs, term, 0, nil, map[string][]string{})
149
+			assert.ErrorContains(t, err, tc.expectedError)
150
+			if tc.shouldReturnError {
151
+				assert.Check(t, cerrdefs.IsUnknown(err), "got: %T: %v", err, err)
152
+				return
153
+			}
154
+			assert.Check(t, cerrdefs.IsInvalidArgument(err), "got: %T: %v", err, err)
155
+		})
156
+	}
157
+}
158
+
159
+func TestSearch(t *testing.T) {
160
+	const term = "term"
161
+	successCases := []struct {
162
+		name            string
163
+		filtersArgs     filters.Args
164
+		registryResults []registry.SearchResult
165
+		expectedResults []registry.SearchResult
166
+	}{
167
+		{
168
+			name:            "empty results",
169
+			registryResults: []registry.SearchResult{},
170
+			expectedResults: []registry.SearchResult{},
171
+		},
172
+		{
173
+			name: "no filter",
174
+			registryResults: []registry.SearchResult{
175
+				{
176
+					Name:        "name",
177
+					Description: "description",
178
+				},
179
+			},
180
+			expectedResults: []registry.SearchResult{
181
+				{
182
+					Name:        "name",
183
+					Description: "description",
184
+				},
185
+			},
186
+		},
187
+		{
188
+			name:        "is-automated=true, no results",
189
+			filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")),
190
+			registryResults: []registry.SearchResult{
191
+				{
192
+					Name:        "name",
193
+					Description: "description",
194
+				},
195
+			},
196
+			expectedResults: []registry.SearchResult{},
197
+		},
198
+		{
199
+			name:        "is-automated=true",
200
+			filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")),
201
+			registryResults: []registry.SearchResult{
202
+				{
203
+					Name:        "name",
204
+					Description: "description",
205
+					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
206
+				},
207
+			},
208
+			expectedResults: []registry.SearchResult{},
209
+		},
210
+		{
211
+			name:        "is-automated=false, IsAutomated reset to false",
212
+			filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")),
213
+			registryResults: []registry.SearchResult{
214
+				{
215
+					Name:        "name",
216
+					Description: "description",
217
+					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
218
+				},
219
+			},
220
+			expectedResults: []registry.SearchResult{
221
+				{
222
+					Name:        "name",
223
+					Description: "description",
224
+					IsAutomated: false, //nolint:staticcheck // ignore SA1019 (field is deprecated).
225
+				},
226
+			},
227
+		},
228
+		{
229
+			name:        "is-automated=false",
230
+			filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")),
231
+			registryResults: []registry.SearchResult{
232
+				{
233
+					Name:        "name",
234
+					Description: "description",
235
+				},
236
+			},
237
+			expectedResults: []registry.SearchResult{
238
+				{
239
+					Name:        "name",
240
+					Description: "description",
241
+				},
242
+			},
243
+		},
244
+		{
245
+			name:        "is-official=true, no results",
246
+			filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")),
247
+			registryResults: []registry.SearchResult{
248
+				{
249
+					Name:        "name",
250
+					Description: "description",
251
+				},
252
+			},
253
+			expectedResults: []registry.SearchResult{},
254
+		},
255
+		{
256
+			name:        "is-official=true",
257
+			filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")),
258
+			registryResults: []registry.SearchResult{
259
+				{
260
+					Name:        "name",
261
+					Description: "description",
262
+					IsOfficial:  true,
263
+				},
264
+			},
265
+			expectedResults: []registry.SearchResult{
266
+				{
267
+					Name:        "name",
268
+					Description: "description",
269
+					IsOfficial:  true,
270
+				},
271
+			},
272
+		},
273
+		{
274
+			name:        "is-official=false, no results",
275
+			filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")),
276
+			registryResults: []registry.SearchResult{
277
+				{
278
+					Name:        "name",
279
+					Description: "description",
280
+					IsOfficial:  true,
281
+				},
282
+			},
283
+			expectedResults: []registry.SearchResult{},
284
+		},
285
+		{
286
+			name:        "is-official=false",
287
+			filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")),
288
+			registryResults: []registry.SearchResult{
289
+				{
290
+					Name:        "name",
291
+					Description: "description",
292
+					IsOfficial:  false,
293
+				},
294
+			},
295
+			expectedResults: []registry.SearchResult{
296
+				{
297
+					Name:        "name",
298
+					Description: "description",
299
+					IsOfficial:  false,
300
+				},
301
+			},
302
+		},
303
+		{
304
+			name:        "stars=0",
305
+			filtersArgs: filters.NewArgs(filters.Arg("stars", "0")),
306
+			registryResults: []registry.SearchResult{
307
+				{
308
+					Name:        "name",
309
+					Description: "description",
310
+					StarCount:   0,
311
+				},
312
+			},
313
+			expectedResults: []registry.SearchResult{
314
+				{
315
+					Name:        "name",
316
+					Description: "description",
317
+					StarCount:   0,
318
+				},
319
+			},
320
+		},
321
+		{
322
+			name:        "stars=0, no results",
323
+			filtersArgs: filters.NewArgs(filters.Arg("stars", "1")),
324
+			registryResults: []registry.SearchResult{
325
+				{
326
+					Name:        "name",
327
+					Description: "description",
328
+					StarCount:   0,
329
+				},
330
+			},
331
+			expectedResults: []registry.SearchResult{},
332
+		},
333
+		{
334
+			name:        "stars=1",
335
+			filtersArgs: filters.NewArgs(filters.Arg("stars", "1")),
336
+			registryResults: []registry.SearchResult{
337
+				{
338
+					Name:        "name0",
339
+					Description: "description0",
340
+					StarCount:   0,
341
+				},
342
+				{
343
+					Name:        "name1",
344
+					Description: "description1",
345
+					StarCount:   1,
346
+				},
347
+			},
348
+			expectedResults: []registry.SearchResult{
349
+				{
350
+					Name:        "name1",
351
+					Description: "description1",
352
+					StarCount:   1,
353
+				},
354
+			},
355
+		},
356
+		{
357
+			name: "stars=1, is-official=true, is-automated=true",
358
+			filtersArgs: filters.NewArgs(
359
+				filters.Arg("stars", "1"),
360
+				filters.Arg("is-official", "true"),
361
+				filters.Arg("is-automated", "true"),
362
+			),
363
+			registryResults: []registry.SearchResult{
364
+				{
365
+					Name:        "name0",
366
+					Description: "description0",
367
+					StarCount:   0,
368
+					IsOfficial:  true,
369
+					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
370
+				},
371
+				{
372
+					Name:        "name1",
373
+					Description: "description1",
374
+					StarCount:   1,
375
+					IsOfficial:  false,
376
+					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
377
+				},
378
+				{
379
+					Name:        "name2",
380
+					Description: "description2",
381
+					StarCount:   1,
382
+					IsOfficial:  true,
383
+				},
384
+				{
385
+					Name:        "name3",
386
+					Description: "description3",
387
+					StarCount:   2,
388
+					IsOfficial:  true,
389
+					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
390
+				},
391
+			},
392
+			expectedResults: []registry.SearchResult{},
393
+		},
394
+	}
395
+	for _, tc := range successCases {
396
+		t.Run(tc.name, func(t *testing.T) {
397
+			srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
398
+				w.Header().Set("Content-type", "application/json")
399
+				json.NewEncoder(w).Encode(registry.SearchResults{
400
+					Query:      term,
401
+					NumResults: len(tc.registryResults),
402
+					Results:    tc.registryResults,
403
+				})
404
+			}))
405
+			defer srv.Close()
406
+
407
+			// Construct the search term by cutting the 'http://' prefix off srv.URL.
408
+			searchTerm := srv.URL[7:] + "/" + term
409
+
410
+			reg, err := NewService(ServiceOptions{})
411
+			assert.NilError(t, err)
412
+			results, err := reg.Search(context.Background(), tc.filtersArgs, searchTerm, 0, nil, map[string][]string{})
413
+			assert.NilError(t, err)
414
+			assert.DeepEqual(t, results, tc.expectedResults)
415
+		})
416
+	}
417
+}
0 418
new file mode 100644
... ...
@@ -0,0 +1,160 @@
0
+package registry
1
+
2
+import (
3
+	"context"
4
+	"crypto/tls"
5
+	"errors"
6
+	"net/url"
7
+	"strings"
8
+	"sync"
9
+
10
+	cerrdefs "github.com/containerd/errdefs"
11
+	"github.com/containerd/log"
12
+	"github.com/distribution/reference"
13
+	"github.com/moby/moby/api/types/registry"
14
+)
15
+
16
+// Service is a registry service. It tracks configuration data such as a list
17
+// of mirrors.
18
+type Service struct {
19
+	config *serviceConfig
20
+	mu     sync.RWMutex
21
+}
22
+
23
+// NewService returns a new instance of [Service] ready to be installed into
24
+// an engine.
25
+func NewService(options ServiceOptions) (*Service, error) {
26
+	config, err := newServiceConfig(options)
27
+	if err != nil {
28
+		return nil, err
29
+	}
30
+
31
+	return &Service{config: config}, err
32
+}
33
+
34
+// ServiceConfig returns a copy of the public registry service's configuration.
35
+func (s *Service) ServiceConfig() *registry.ServiceConfig {
36
+	s.mu.RLock()
37
+	defer s.mu.RUnlock()
38
+	return s.config.copy()
39
+}
40
+
41
+// ReplaceConfig prepares a transaction which will atomically replace the
42
+// registry service's configuration when the returned commit function is called.
43
+func (s *Service) ReplaceConfig(options ServiceOptions) (commit func(), _ error) {
44
+	config, err := newServiceConfig(options)
45
+	if err != nil {
46
+		return nil, err
47
+	}
48
+	return func() {
49
+		s.mu.Lock()
50
+		defer s.mu.Unlock()
51
+		s.config = config
52
+	}, nil
53
+}
54
+
55
+// Auth contacts the public registry with the provided credentials,
56
+// and returns OK if authentication was successful.
57
+// It can be used to verify the validity of a client's credentials.
58
+func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (statusMessage, token string, _ error) {
59
+	// TODO Use ctx when searching for repositories
60
+	registryHostName := IndexHostname
61
+
62
+	if authConfig.ServerAddress != "" {
63
+		serverAddress := authConfig.ServerAddress
64
+		if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") {
65
+			serverAddress = "https://" + serverAddress
66
+		}
67
+		u, err := url.Parse(serverAddress)
68
+		if err != nil {
69
+			return "", "", invalidParamWrapf(err, "unable to parse server address")
70
+		}
71
+		registryHostName = u.Host
72
+	}
73
+
74
+	// Lookup endpoints for authentication but exclude mirrors to prevent
75
+	// sending credentials of the upstream registry to a mirror.
76
+	s.mu.RLock()
77
+	endpoints, err := s.lookupV2Endpoints(ctx, registryHostName, false)
78
+	s.mu.RUnlock()
79
+	if err != nil {
80
+		if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
81
+			return "", "", err
82
+		}
83
+		return "", "", invalidParam(err)
84
+	}
85
+
86
+	var lastErr error
87
+	for _, endpoint := range endpoints {
88
+		authToken, err := loginV2(ctx, authConfig, endpoint, userAgent)
89
+		if err != nil {
90
+			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || cerrdefs.IsUnauthorized(err) {
91
+				// Failed to authenticate; don't continue with (non-TLS) endpoints.
92
+				return "", "", err
93
+			}
94
+			// Try next endpoint
95
+			log.G(ctx).WithFields(log.Fields{
96
+				"error":    err,
97
+				"endpoint": endpoint,
98
+			}).Infof("Error logging in to endpoint, trying next endpoint")
99
+			lastErr = err
100
+			continue
101
+		}
102
+
103
+		// TODO(thaJeztah): move the statusMessage to the API endpoint; we don't need to produce that here?
104
+		return "Login Succeeded", authToken, nil
105
+	}
106
+
107
+	return "", "", lastErr
108
+}
109
+
110
+// ResolveAuthConfig looks up authentication for the given reference from the
111
+// given authConfigs.
112
+//
113
+// IMPORTANT: This function is for internal use and should not be used by external projects.
114
+func (s *Service) ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, ref reference.Named) registry.AuthConfig {
115
+	s.mu.RLock()
116
+	defer s.mu.RUnlock()
117
+	// Simplified version of "newIndexInfo" without handling of insecure
118
+	// registries and mirrors, as we don't need that information to resolve
119
+	// the auth-config.
120
+	indexName := normalizeIndexName(reference.Domain(ref))
121
+	registryInfo, ok := s.config.IndexConfigs[indexName]
122
+	if !ok {
123
+		registryInfo = &registry.IndexInfo{Name: indexName}
124
+	}
125
+	return ResolveAuthConfig(authConfigs, registryInfo)
126
+}
127
+
128
+// APIEndpoint represents a remote API endpoint
129
+type APIEndpoint struct {
130
+	Mirror    bool
131
+	URL       *url.URL
132
+	TLSConfig *tls.Config
133
+}
134
+
135
+// LookupPullEndpoints creates a list of v2 endpoints to try to pull from, in order of preference.
136
+// It gives preference to mirrors over the actual registry, and HTTPS over plain HTTP.
137
+func (s *Service) LookupPullEndpoints(hostname string) ([]APIEndpoint, error) {
138
+	s.mu.RLock()
139
+	defer s.mu.RUnlock()
140
+
141
+	return s.lookupV2Endpoints(context.TODO(), hostname, true)
142
+}
143
+
144
+// LookupPushEndpoints creates a list of v2 endpoints to try to push to, in order of preference.
145
+// It gives preference to HTTPS over plain HTTP. Mirrors are not included.
146
+func (s *Service) LookupPushEndpoints(hostname string) ([]APIEndpoint, error) {
147
+	s.mu.RLock()
148
+	defer s.mu.RUnlock()
149
+
150
+	return s.lookupV2Endpoints(context.TODO(), hostname, false)
151
+}
152
+
153
+// IsInsecureRegistry returns true if the registry at given host is configured as
154
+// insecure registry.
155
+func (s *Service) IsInsecureRegistry(host string) bool {
156
+	s.mu.RLock()
157
+	defer s.mu.RUnlock()
158
+	return !s.config.isSecureIndex(host)
159
+}
0 160
new file mode 100644
... ...
@@ -0,0 +1,73 @@
0
+package registry
1
+
2
+import (
3
+	"context"
4
+	"net/url"
5
+	"strings"
6
+
7
+	"github.com/docker/go-connections/tlsconfig"
8
+)
9
+
10
+func (s *Service) lookupV2Endpoints(ctx context.Context, hostname string, includeMirrors bool) ([]APIEndpoint, error) {
11
+	var endpoints []APIEndpoint
12
+	if hostname == DefaultNamespace || hostname == IndexHostname {
13
+		if includeMirrors {
14
+			for _, mirror := range s.config.Mirrors {
15
+				if ctx.Err() != nil {
16
+					return nil, ctx.Err()
17
+				}
18
+				if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {
19
+					mirror = "https://" + mirror
20
+				}
21
+				mirrorURL, err := url.Parse(mirror)
22
+				if err != nil {
23
+					return nil, invalidParam(err)
24
+				}
25
+				// TODO(thaJeztah); this should all be memoized when loading the config. We're resolving mirrors and loading TLS config every time.
26
+				mirrorTLSConfig, err := newTLSConfig(ctx, mirrorURL.Host, s.config.isSecureIndex(mirrorURL.Host))
27
+				if err != nil {
28
+					return nil, err
29
+				}
30
+				endpoints = append(endpoints, APIEndpoint{
31
+					URL:       mirrorURL,
32
+					Mirror:    true,
33
+					TLSConfig: mirrorTLSConfig,
34
+				})
35
+			}
36
+		}
37
+		endpoints = append(endpoints, APIEndpoint{
38
+			URL:       DefaultV2Registry,
39
+			TLSConfig: tlsconfig.ServerDefault(),
40
+		})
41
+
42
+		return endpoints, nil
43
+	}
44
+
45
+	tlsConfig, err := newTLSConfig(ctx, hostname, s.config.isSecureIndex(hostname))
46
+	if err != nil {
47
+		return nil, err
48
+	}
49
+
50
+	endpoints = []APIEndpoint{
51
+		{
52
+			URL: &url.URL{
53
+				Scheme: "https",
54
+				Host:   hostname,
55
+			},
56
+			TLSConfig: tlsConfig,
57
+		},
58
+	}
59
+
60
+	if tlsConfig.InsecureSkipVerify {
61
+		endpoints = append(endpoints, APIEndpoint{
62
+			URL: &url.URL{
63
+				Scheme: "http",
64
+				Host:   hostname,
65
+			},
66
+			// used to check if supposed to be secure via InsecureSkipVerify
67
+			TLSConfig: tlsConfig,
68
+		})
69
+	}
70
+
71
+	return endpoints, nil
72
+}
0 73
new file mode 100644
... ...
@@ -0,0 +1,13 @@
0
+package registry
1
+
2
+import (
3
+	"github.com/distribution/reference"
4
+	"github.com/moby/moby/api/types/registry"
5
+)
6
+
7
+// RepositoryInfo describes a repository
8
+type RepositoryInfo struct {
9
+	Name reference.Named
10
+	// Index points to registry information
11
+	Index *registry.IndexInfo
12
+}
... ...
@@ -9,7 +9,7 @@ import (
9 9
 	"github.com/docker/docker/daemon/config"
10 10
 	"github.com/docker/docker/daemon/images"
11 11
 	"github.com/docker/docker/daemon/libnetwork"
12
-	"github.com/docker/docker/registry"
12
+	"github.com/docker/docker/daemon/pkg/registry"
13 13
 	"gotest.tools/v3/assert"
14 14
 )
15 15
 
... ...
@@ -4,8 +4,8 @@ import (
4 4
 	"fmt"
5 5
 	"testing"
6 6
 
7
+	registrypkg "github.com/docker/docker/daemon/pkg/registry"
7 8
 	"github.com/docker/docker/integration/internal/requirement"
8
-	registrypkg "github.com/docker/docker/registry"
9 9
 	"github.com/moby/moby/api/types/registry"
10 10
 	"gotest.tools/v3/assert"
11 11
 	is "gotest.tools/v3/assert/cmp"
12 12
deleted file mode 100644
... ...
@@ -1,205 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"context"
5
-	"net/http"
6
-	"net/url"
7
-	"strings"
8
-	"time"
9
-
10
-	"github.com/containerd/log"
11
-	"github.com/docker/distribution/registry/client/auth"
12
-	"github.com/docker/distribution/registry/client/auth/challenge"
13
-	"github.com/docker/distribution/registry/client/transport"
14
-	"github.com/moby/moby/api/types/registry"
15
-	"github.com/pkg/errors"
16
-)
17
-
18
-// AuthClientID is used the ClientID used for the token server
19
-const AuthClientID = "docker"
20
-
21
-type loginCredentialStore struct {
22
-	authConfig *registry.AuthConfig
23
-}
24
-
25
-func (lcs loginCredentialStore) Basic(*url.URL) (string, string) {
26
-	return lcs.authConfig.Username, lcs.authConfig.Password
27
-}
28
-
29
-func (lcs loginCredentialStore) RefreshToken(*url.URL, string) string {
30
-	return lcs.authConfig.IdentityToken
31
-}
32
-
33
-func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token string) {
34
-	lcs.authConfig.IdentityToken = token
35
-}
36
-
37
-type staticCredentialStore struct {
38
-	auth *registry.AuthConfig
39
-}
40
-
41
-// NewStaticCredentialStore returns a credential store
42
-// which always returns the same credential values.
43
-func NewStaticCredentialStore(ac *registry.AuthConfig) auth.CredentialStore {
44
-	return staticCredentialStore{
45
-		auth: ac,
46
-	}
47
-}
48
-
49
-func (scs staticCredentialStore) Basic(*url.URL) (string, string) {
50
-	if scs.auth == nil {
51
-		return "", ""
52
-	}
53
-	return scs.auth.Username, scs.auth.Password
54
-}
55
-
56
-func (scs staticCredentialStore) RefreshToken(*url.URL, string) string {
57
-	if scs.auth == nil {
58
-		return ""
59
-	}
60
-	return scs.auth.IdentityToken
61
-}
62
-
63
-func (staticCredentialStore) SetRefreshToken(*url.URL, string, string) {
64
-}
65
-
66
-// loginV2 tries to login to the v2 registry server. The given registry
67
-// endpoint will be pinged to get authorization challenges. These challenges
68
-// will be used to authenticate against the registry to validate credentials.
69
-func loginV2(ctx context.Context, authConfig *registry.AuthConfig, endpoint APIEndpoint, userAgent string) (token string, _ error) {
70
-	endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
71
-	log.G(ctx).WithField("endpoint", endpointStr).Debug("attempting v2 login to registry endpoint")
72
-
73
-	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpointStr, http.NoBody)
74
-	if err != nil {
75
-		return "", err
76
-	}
77
-
78
-	var (
79
-		modifiers            = Headers(userAgent, nil)
80
-		authTrans            = transport.NewTransport(newTransport(endpoint.TLSConfig), modifiers...)
81
-		credentialAuthConfig = *authConfig
82
-		creds                = loginCredentialStore{authConfig: &credentialAuthConfig}
83
-	)
84
-
85
-	loginClient, err := v2AuthHTTPClient(endpoint.URL, authTrans, modifiers, creds, nil)
86
-	if err != nil {
87
-		return "", err
88
-	}
89
-
90
-	resp, err := loginClient.Do(req)
91
-	if err != nil {
92
-		err = translateV2AuthError(err)
93
-		return "", err
94
-	}
95
-	defer resp.Body.Close()
96
-
97
-	if resp.StatusCode != http.StatusOK {
98
-		// TODO(dmcgowan): Attempt to further interpret result, status code and error code string
99
-		return "", errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
100
-	}
101
-
102
-	return credentialAuthConfig.IdentityToken, nil
103
-}
104
-
105
-func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, error) {
106
-	challengeManager, err := PingV2Registry(endpoint, authTransport)
107
-	if err != nil {
108
-		return nil, err
109
-	}
110
-
111
-	authHandlers := []auth.AuthenticationHandler{
112
-		auth.NewTokenHandlerWithOptions(auth.TokenHandlerOptions{
113
-			Transport:     authTransport,
114
-			Credentials:   creds,
115
-			OfflineAccess: true,
116
-			ClientID:      AuthClientID,
117
-			Scopes:        scopes,
118
-		}),
119
-		auth.NewBasicHandler(creds),
120
-	}
121
-
122
-	modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, authHandlers...))
123
-
124
-	return &http.Client{
125
-		Transport: transport.NewTransport(authTransport, modifiers...),
126
-		Timeout:   15 * time.Second,
127
-	}, nil
128
-}
129
-
130
-// ConvertToHostname normalizes a registry URL which has http|https prepended
131
-// to just its hostname. It is used to match credentials, which may be either
132
-// stored as hostname or as hostname including scheme (in legacy configuration
133
-// files).
134
-func ConvertToHostname(maybeURL string) string {
135
-	stripped := maybeURL
136
-	if scheme, remainder, ok := strings.Cut(stripped, "://"); ok {
137
-		switch scheme {
138
-		case "http", "https":
139
-			stripped = remainder
140
-		default:
141
-			// unknown, or no scheme; doing nothing for now, as we never did.
142
-		}
143
-	}
144
-	stripped, _, _ = strings.Cut(stripped, "/")
145
-	return stripped
146
-}
147
-
148
-// ResolveAuthConfig matches an auth configuration to a server address or a URL
149
-func ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, index *registry.IndexInfo) registry.AuthConfig {
150
-	configKey := GetAuthConfigKey(index)
151
-	// First try the happy case
152
-	if c, found := authConfigs[configKey]; found || index.Official {
153
-		return c
154
-	}
155
-
156
-	// Maybe they have a legacy config file, we will iterate the keys converting
157
-	// them to the new format and testing
158
-	for registryURL, ac := range authConfigs {
159
-		if configKey == ConvertToHostname(registryURL) {
160
-			return ac
161
-		}
162
-	}
163
-
164
-	// When all else fails, return an empty auth config
165
-	return registry.AuthConfig{}
166
-}
167
-
168
-// PingResponseError is used when the response from a ping
169
-// was received but invalid.
170
-type PingResponseError struct {
171
-	Err error
172
-}
173
-
174
-func (err PingResponseError) Error() string {
175
-	return err.Err.Error()
176
-}
177
-
178
-// PingV2Registry attempts to ping a v2 registry and on success return a
179
-// challenge manager for the supported authentication types.
180
-// If a response is received but cannot be interpreted, a PingResponseError will be returned.
181
-func PingV2Registry(endpoint *url.URL, authTransport http.RoundTripper) (challenge.Manager, error) {
182
-	pingClient := &http.Client{
183
-		Transport: authTransport,
184
-		Timeout:   15 * time.Second,
185
-	}
186
-	endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/"
187
-	req, err := http.NewRequest(http.MethodGet, endpointStr, http.NoBody)
188
-	if err != nil {
189
-		return nil, err
190
-	}
191
-	resp, err := pingClient.Do(req)
192
-	if err != nil {
193
-		return nil, err
194
-	}
195
-	defer resp.Body.Close()
196
-
197
-	challengeManager := challenge.NewSimpleManager()
198
-	if err := challengeManager.AddResponse(resp); err != nil {
199
-		return nil, PingResponseError{
200
-			Err: err,
201
-		}
202
-	}
203
-
204
-	return challengeManager, nil
205
-}
206 1
deleted file mode 100644
... ...
@@ -1,106 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"testing"
5
-
6
-	"github.com/moby/moby/api/types/registry"
7
-	"gotest.tools/v3/assert"
8
-)
9
-
10
-func buildAuthConfigs() map[string]registry.AuthConfig {
11
-	authConfigs := map[string]registry.AuthConfig{}
12
-
13
-	for _, reg := range []string{"testIndex", IndexServer} {
14
-		authConfigs[reg] = registry.AuthConfig{
15
-			Username: "docker-user",
16
-			Password: "docker-pass",
17
-		}
18
-	}
19
-
20
-	return authConfigs
21
-}
22
-
23
-func TestResolveAuthConfigIndexServer(t *testing.T) {
24
-	authConfigs := buildAuthConfigs()
25
-	indexConfig := authConfigs[IndexServer]
26
-
27
-	officialIndex := &registry.IndexInfo{
28
-		Official: true,
29
-	}
30
-	privateIndex := &registry.IndexInfo{
31
-		Official: false,
32
-	}
33
-
34
-	resolved := ResolveAuthConfig(authConfigs, officialIndex)
35
-	assert.Equal(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServer")
36
-
37
-	resolved = ResolveAuthConfig(authConfigs, privateIndex)
38
-	assert.Check(t, resolved != indexConfig, "Expected ResolveAuthConfig to not return IndexServer")
39
-}
40
-
41
-func TestResolveAuthConfigFullURL(t *testing.T) {
42
-	authConfigs := buildAuthConfigs()
43
-
44
-	registryAuth := registry.AuthConfig{
45
-		Username: "foo-user",
46
-		Password: "foo-pass",
47
-	}
48
-	localAuth := registry.AuthConfig{
49
-		Username: "bar-user",
50
-		Password: "bar-pass",
51
-	}
52
-	officialAuth := registry.AuthConfig{
53
-		Username: "baz-user",
54
-		Password: "baz-pass",
55
-	}
56
-	authConfigs[IndexServer] = officialAuth
57
-
58
-	expectedAuths := map[string]registry.AuthConfig{
59
-		"registry.example.com": registryAuth,
60
-		"localhost:8000":       localAuth,
61
-		"example.com":          localAuth,
62
-	}
63
-
64
-	validRegistries := map[string][]string{
65
-		"registry.example.com": {
66
-			"https://registry.example.com/v1/",
67
-			"http://registry.example.com/v1/",
68
-			"registry.example.com",
69
-			"registry.example.com/v1/",
70
-		},
71
-		"localhost:8000": {
72
-			"https://localhost:8000/v1/",
73
-			"http://localhost:8000/v1/",
74
-			"localhost:8000",
75
-			"localhost:8000/v1/",
76
-		},
77
-		"example.com": {
78
-			"https://example.com/v1/",
79
-			"http://example.com/v1/",
80
-			"example.com",
81
-			"example.com/v1/",
82
-		},
83
-	}
84
-
85
-	for configKey, registries := range validRegistries {
86
-		configured, ok := expectedAuths[configKey]
87
-		if !ok {
88
-			t.Fail()
89
-		}
90
-		index := &registry.IndexInfo{
91
-			Name: configKey,
92
-		}
93
-		for _, reg := range registries {
94
-			authConfigs[reg] = configured
95
-			resolved := ResolveAuthConfig(authConfigs, index)
96
-			if resolved.Username != configured.Username || resolved.Password != configured.Password {
97
-				t.Errorf("%s -> %v != %v\n", reg, resolved, configured)
98
-			}
99
-			delete(authConfigs, reg)
100
-			resolved = ResolveAuthConfig(authConfigs, index)
101
-			if resolved.Username == configured.Username || resolved.Password == configured.Password {
102
-				t.Errorf("%s -> %v == %v\n", reg, resolved, configured)
103
-			}
104
-		}
105
-	}
106
-}
107 1
deleted file mode 100644
... ...
@@ -1,449 +0,0 @@
1
-// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
2
-//go:build go1.23
3
-
4
-package registry
5
-
6
-import (
7
-	"context"
8
-	"net"
9
-	"net/url"
10
-	"os"
11
-	"path/filepath"
12
-	"regexp"
13
-	"runtime"
14
-	"strconv"
15
-	"strings"
16
-	"sync"
17
-
18
-	"github.com/containerd/log"
19
-	"github.com/distribution/reference"
20
-	"github.com/moby/moby/api/types/registry"
21
-)
22
-
23
-// ServiceOptions holds command line options.
24
-type ServiceOptions struct {
25
-	Mirrors            []string `json:"registry-mirrors,omitempty"`
26
-	InsecureRegistries []string `json:"insecure-registries,omitempty"`
27
-}
28
-
29
-// serviceConfig holds daemon configuration for the registry service.
30
-type serviceConfig registry.ServiceConfig
31
-
32
-// TODO(thaJeztah) both the "index.docker.io" and "registry-1.docker.io" domains
33
-// are here for historic reasons and backward-compatibility. These domains
34
-// are still supported by Docker Hub (and will continue to be supported), but
35
-// there are new domains already in use, and plans to consolidate all legacy
36
-// domains to new "canonical" domains. Once those domains are decided on, we
37
-// should update these consts (but making sure to preserve compatibility with
38
-// existing installs, clients, and user configuration).
39
-const (
40
-	// DefaultNamespace is the default namespace
41
-	DefaultNamespace = "docker.io"
42
-	// DefaultRegistryHost is the hostname for the default (Docker Hub) registry
43
-	// used for pushing and pulling images. This hostname is hard-coded to handle
44
-	// the conversion from image references without registry name (e.g. "ubuntu",
45
-	// or "ubuntu:latest"), as well as references using the "docker.io" domain
46
-	// name, which is used as canonical reference for images on Docker Hub, but
47
-	// does not match the domain-name of Docker Hub's registry.
48
-	DefaultRegistryHost = "registry-1.docker.io"
49
-	// IndexHostname is the index hostname, used for authentication and image search.
50
-	IndexHostname = "index.docker.io"
51
-	// IndexServer is used for user auth and image search
52
-	IndexServer = "https://" + IndexHostname + "/v1/"
53
-	// IndexName is the name of the index
54
-	IndexName = "docker.io"
55
-)
56
-
57
-var (
58
-	// DefaultV2Registry is the URI of the default (Docker Hub) registry.
59
-	DefaultV2Registry = &url.URL{
60
-		Scheme: "https",
61
-		Host:   DefaultRegistryHost,
62
-	}
63
-
64
-	validHostPortRegex = sync.OnceValue(func() *regexp.Regexp {
65
-		return regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`)
66
-	})
67
-)
68
-
69
-// runningWithRootlessKit is a fork of [rootless.RunningWithRootlessKit],
70
-// but inlining it to prevent adding that as a dependency for docker/cli.
71
-//
72
-// [rootless.RunningWithRootlessKit]: https://github.com/moby/moby/blob/b4bdf12daec84caaf809a639f923f7370d4926ad/pkg/rootless/rootless.go#L5-L8
73
-func runningWithRootlessKit() bool {
74
-	return runtime.GOOS == "linux" && os.Getenv("ROOTLESSKIT_STATE_DIR") != ""
75
-}
76
-
77
-// CertsDir is the directory where certificates are stored.
78
-//
79
-// - Linux: "/etc/docker/certs.d/"
80
-// - Linux (with rootlessKit): $XDG_CONFIG_HOME/docker/certs.d/" or "$HOME/.config/docker/certs.d/"
81
-// - Windows: "%PROGRAMDATA%/docker/certs.d/"
82
-//
83
-// TODO(thaJeztah): certsDir but stored in our config, and passed when needed. For the CLI, we should also default to same path as rootless.
84
-func CertsDir() string {
85
-	certsDir := "/etc/docker/certs.d"
86
-	if runningWithRootlessKit() {
87
-		if configHome, _ := os.UserConfigDir(); configHome != "" {
88
-			certsDir = filepath.Join(configHome, "docker", "certs.d")
89
-		}
90
-	} else if runtime.GOOS == "windows" {
91
-		certsDir = filepath.Join(os.Getenv("programdata"), "docker", "certs.d")
92
-	}
93
-	return certsDir
94
-}
95
-
96
-// newServiceConfig returns a new instance of ServiceConfig
97
-func newServiceConfig(options ServiceOptions) (*serviceConfig, error) {
98
-	config := &serviceConfig{}
99
-	if err := config.loadMirrors(options.Mirrors); err != nil {
100
-		return nil, err
101
-	}
102
-	if err := config.loadInsecureRegistries(options.InsecureRegistries); err != nil {
103
-		return nil, err
104
-	}
105
-
106
-	return config, nil
107
-}
108
-
109
-// copy constructs a new ServiceConfig with a copy of the configuration in config.
110
-func (config *serviceConfig) copy() *registry.ServiceConfig {
111
-	ic := make(map[string]*registry.IndexInfo)
112
-	for key, value := range config.IndexConfigs {
113
-		ic[key] = value
114
-	}
115
-	return &registry.ServiceConfig{
116
-		InsecureRegistryCIDRs: append([]*registry.NetIPNet(nil), config.InsecureRegistryCIDRs...),
117
-		IndexConfigs:          ic,
118
-		Mirrors:               append([]string(nil), config.Mirrors...),
119
-	}
120
-}
121
-
122
-// loadMirrors loads mirrors to config, after removing duplicates.
123
-// Returns an error if mirrors contains an invalid mirror.
124
-func (config *serviceConfig) loadMirrors(mirrors []string) error {
125
-	mMap := map[string]struct{}{}
126
-	unique := []string{}
127
-
128
-	for _, mirror := range mirrors {
129
-		m, err := ValidateMirror(mirror)
130
-		if err != nil {
131
-			return err
132
-		}
133
-		if _, exist := mMap[m]; !exist {
134
-			mMap[m] = struct{}{}
135
-			unique = append(unique, m)
136
-		}
137
-	}
138
-
139
-	config.Mirrors = unique
140
-
141
-	// Configure public registry since mirrors may have changed.
142
-	config.IndexConfigs = map[string]*registry.IndexInfo{
143
-		IndexName: {
144
-			Name:     IndexName,
145
-			Mirrors:  unique,
146
-			Secure:   true,
147
-			Official: true,
148
-		},
149
-	}
150
-
151
-	return nil
152
-}
153
-
154
-// loadInsecureRegistries loads insecure registries to config
155
-func (config *serviceConfig) loadInsecureRegistries(registries []string) error {
156
-	// Localhost is by default considered as an insecure registry. This is a
157
-	// stop-gap for people who are running a private registry on localhost.
158
-	registries = append(registries, "::1/128", "127.0.0.0/8")
159
-
160
-	var (
161
-		insecureRegistryCIDRs = make([]*registry.NetIPNet, 0)
162
-		indexConfigs          = make(map[string]*registry.IndexInfo)
163
-	)
164
-
165
-skip:
166
-	for _, r := range registries {
167
-		// validate insecure registry
168
-		if _, err := ValidateIndexName(r); err != nil {
169
-			return err
170
-		}
171
-		if scheme, host, ok := strings.Cut(r, "://"); ok {
172
-			switch strings.ToLower(scheme) {
173
-			case "http", "https":
174
-				log.G(context.TODO()).Warnf("insecure registry %[1]s should not contain '%[2]s' and '%[2]ss' has been removed from the insecure registry config", r, scheme)
175
-				r = host
176
-			default:
177
-				// unsupported scheme
178
-				return invalidParamf("insecure registry %s should not contain '://'", r)
179
-			}
180
-		}
181
-		// Check if CIDR was passed to --insecure-registry
182
-		_, ipnet, err := net.ParseCIDR(r)
183
-		if err == nil {
184
-			// Valid CIDR. If ipnet is already in config.InsecureRegistryCIDRs, skip.
185
-			data := (*registry.NetIPNet)(ipnet)
186
-			for _, value := range insecureRegistryCIDRs {
187
-				if value.IP.String() == data.IP.String() && value.Mask.String() == data.Mask.String() {
188
-					continue skip
189
-				}
190
-			}
191
-			// ipnet is not found, add it in config.InsecureRegistryCIDRs
192
-			insecureRegistryCIDRs = append(insecureRegistryCIDRs, data)
193
-		} else {
194
-			if err := validateHostPort(r); err != nil {
195
-				return invalidParamWrapf(err, "insecure registry %s is not valid", r)
196
-			}
197
-			// Assume `host:port` if not CIDR.
198
-			indexConfigs[r] = &registry.IndexInfo{
199
-				Name:     r,
200
-				Mirrors:  []string{},
201
-				Secure:   false,
202
-				Official: false,
203
-			}
204
-		}
205
-	}
206
-
207
-	// Configure public registry.
208
-	indexConfigs[IndexName] = &registry.IndexInfo{
209
-		Name:     IndexName,
210
-		Mirrors:  config.Mirrors,
211
-		Secure:   true,
212
-		Official: true,
213
-	}
214
-	config.InsecureRegistryCIDRs = insecureRegistryCIDRs
215
-	config.IndexConfigs = indexConfigs
216
-
217
-	return nil
218
-}
219
-
220
-// isSecureIndex returns false if the provided indexName is part of the list of insecure registries
221
-// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs.
222
-//
223
-// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet.
224
-// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered
225
-// insecure.
226
-//
227
-// indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name
228
-// or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained
229
-// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element
230
-// of insecureRegistries.
231
-func (config *serviceConfig) isSecureIndex(indexName string) bool {
232
-	// Check for configured index, first.  This is needed in case isSecureIndex
233
-	// is called from anything besides newIndexInfo, in order to honor per-index configurations.
234
-	if index, ok := config.IndexConfigs[indexName]; ok {
235
-		return index.Secure
236
-	}
237
-
238
-	return !isCIDRMatch(config.InsecureRegistryCIDRs, indexName)
239
-}
240
-
241
-// for mocking in unit tests.
242
-var lookupIP = net.LookupIP
243
-
244
-// isCIDRMatch returns true if urlHost matches an element of cidrs. urlHost is a URL.Host ("host:port" or "host")
245
-// where the `host` part can be either a domain name or an IP address. If it is a domain name, then it will be
246
-// resolved to IP addresses for matching. If resolution fails, false is returned.
247
-func isCIDRMatch(cidrs []*registry.NetIPNet, urlHost string) bool {
248
-	if len(cidrs) == 0 {
249
-		return false
250
-	}
251
-
252
-	host, _, err := net.SplitHostPort(urlHost)
253
-	if err != nil {
254
-		// Assume urlHost is a host without port and go on.
255
-		host = urlHost
256
-	}
257
-
258
-	var addresses []net.IP
259
-	if ip := net.ParseIP(host); ip != nil {
260
-		// Host is an IP-address.
261
-		addresses = append(addresses, ip)
262
-	} else {
263
-		// Try to resolve the host's IP-address.
264
-		addresses, err = lookupIP(host)
265
-		if err != nil {
266
-			// We failed to resolve the host; assume there's no match.
267
-			return false
268
-		}
269
-	}
270
-
271
-	for _, addr := range addresses {
272
-		for _, ipnet := range cidrs {
273
-			// check if the addr falls in the subnet
274
-			if (*net.IPNet)(ipnet).Contains(addr) {
275
-				return true
276
-			}
277
-		}
278
-	}
279
-
280
-	return false
281
-}
282
-
283
-// ValidateMirror validates and normalizes an HTTP(S) registry mirror. It
284
-// returns an error if the given mirrorURL is invalid, or the normalized
285
-// format for the URL otherwise.
286
-//
287
-// It is used by the daemon to validate the daemon configuration.
288
-func ValidateMirror(mirrorURL string) (string, error) {
289
-	// Fast path for missing scheme, as url.Parse splits by ":", which can
290
-	// cause the hostname to be considered the "scheme" when using "hostname:port".
291
-	if scheme, _, ok := strings.Cut(mirrorURL, "://"); !ok || scheme == "" {
292
-		return "", invalidParamf("invalid mirror: no scheme specified for %q: must use either 'https://' or 'http://'", mirrorURL)
293
-	}
294
-	uri, err := url.Parse(mirrorURL)
295
-	if err != nil {
296
-		return "", invalidParamWrapf(err, "invalid mirror: %q is not a valid URI", mirrorURL)
297
-	}
298
-	if uri.Scheme != "http" && uri.Scheme != "https" {
299
-		return "", invalidParamf("invalid mirror: unsupported scheme %q in %q: must use either 'https://' or 'http://'", uri.Scheme, uri)
300
-	}
301
-	if uri.RawQuery != "" || uri.Fragment != "" {
302
-		return "", invalidParamf("invalid mirror: query or fragment at end of the URI %q", uri)
303
-	}
304
-	if uri.User != nil {
305
-		// strip password from output
306
-		uri.User = url.UserPassword(uri.User.Username(), "xxxxx")
307
-		return "", invalidParamf("invalid mirror: username/password not allowed in URI %q", uri)
308
-	}
309
-	return strings.TrimSuffix(mirrorURL, "/") + "/", nil
310
-}
311
-
312
-// ValidateIndexName validates an index name. It is used by the daemon to
313
-// validate the daemon configuration.
314
-func ValidateIndexName(val string) (string, error) {
315
-	val = normalizeIndexName(val)
316
-	if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") {
317
-		return "", invalidParamf("invalid index name (%s). Cannot begin or end with a hyphen", val)
318
-	}
319
-	return val, nil
320
-}
321
-
322
-func normalizeIndexName(val string) string {
323
-	// TODO(thaJeztah): consider normalizing other known options, such as "(https://)registry-1.docker.io", "https://index.docker.io/v1/".
324
-	// TODO: upstream this to check to reference package
325
-	if val == "index.docker.io" {
326
-		return "docker.io"
327
-	}
328
-	return val
329
-}
330
-
331
-func hasScheme(reposName string) bool {
332
-	return strings.Contains(reposName, "://")
333
-}
334
-
335
-func validateHostPort(s string) error {
336
-	// Split host and port, and in case s can not be split, assume host only
337
-	host, port, err := net.SplitHostPort(s)
338
-	if err != nil {
339
-		host = s
340
-		port = ""
341
-	}
342
-	// If match against the `host:port` pattern fails,
343
-	// it might be `IPv6:port`, which will be captured by net.ParseIP(host)
344
-	if !validHostPortRegex().MatchString(s) && net.ParseIP(host) == nil {
345
-		return invalidParamf("invalid host %q", host)
346
-	}
347
-	if port != "" {
348
-		v, err := strconv.Atoi(port)
349
-		if err != nil {
350
-			return err
351
-		}
352
-		if v < 0 || v > 65535 {
353
-			return invalidParamf("invalid port %q", port)
354
-		}
355
-	}
356
-	return nil
357
-}
358
-
359
-// newIndexInfo returns IndexInfo configuration from indexName
360
-func newIndexInfo(config *serviceConfig, indexName string) *registry.IndexInfo {
361
-	indexName = normalizeIndexName(indexName)
362
-
363
-	// Return any configured index info, first.
364
-	if index, ok := config.IndexConfigs[indexName]; ok {
365
-		return index
366
-	}
367
-
368
-	// Construct a non-configured index info.
369
-	return &registry.IndexInfo{
370
-		Name:    indexName,
371
-		Mirrors: []string{},
372
-		Secure:  config.isSecureIndex(indexName),
373
-	}
374
-}
375
-
376
-// GetAuthConfigKey special-cases using the full index address of the official
377
-// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
378
-func GetAuthConfigKey(index *registry.IndexInfo) string {
379
-	if index.Official {
380
-		return IndexServer
381
-	}
382
-	return index.Name
383
-}
384
-
385
-// ParseRepositoryInfo performs the breakdown of a repository name into a
386
-// [RepositoryInfo], but lacks registry configuration.
387
-//
388
-// It is used by the Docker cli to interact with registry-related endpoints.
389
-func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
390
-	indexName := normalizeIndexName(reference.Domain(reposName))
391
-	if indexName == IndexName {
392
-		return &RepositoryInfo{
393
-			Name: reference.TrimNamed(reposName),
394
-			Index: &registry.IndexInfo{
395
-				Name:     IndexName,
396
-				Mirrors:  []string{},
397
-				Secure:   true,
398
-				Official: true,
399
-			},
400
-		}, nil
401
-	}
402
-
403
-	return &RepositoryInfo{
404
-		Name: reference.TrimNamed(reposName),
405
-		Index: &registry.IndexInfo{
406
-			Name:    indexName,
407
-			Mirrors: []string{},
408
-			Secure:  !isInsecure(indexName),
409
-		},
410
-	}, nil
411
-}
412
-
413
-// isInsecure is used to detect whether a registry domain or IP-address is allowed
414
-// to use an insecure (non-TLS, or self-signed cert) connection according to the
415
-// defaults, which allows for insecure connections with registries running on a
416
-// loopback address ("localhost", "::1/128", "127.0.0.0/8").
417
-//
418
-// It is used in situations where we don't have access to the daemon's configuration,
419
-// for example, when used from the client / CLI.
420
-func isInsecure(hostNameOrIP string) bool {
421
-	// Attempt to strip port if present; this also strips brackets for
422
-	// IPv6 addresses with a port (e.g. "[::1]:5000").
423
-	//
424
-	// This is best-effort; we'll continue using the address as-is if it fails.
425
-	if host, _, err := net.SplitHostPort(hostNameOrIP); err == nil {
426
-		hostNameOrIP = host
427
-	}
428
-	if hostNameOrIP == "127.0.0.1" || hostNameOrIP == "::1" || strings.EqualFold(hostNameOrIP, "localhost") {
429
-		// Fast path; no need to resolve these, assuming nobody overrides
430
-		// "localhost" for anything else than a loopback address (sorry, not sorry).
431
-		return true
432
-	}
433
-
434
-	var addresses []net.IP
435
-	if ip := net.ParseIP(hostNameOrIP); ip != nil {
436
-		addresses = append(addresses, ip)
437
-	} else {
438
-		// Try to resolve the host's IP-addresses.
439
-		addrs, _ := lookupIP(hostNameOrIP)
440
-		addresses = append(addresses, addrs...)
441
-	}
442
-
443
-	for _, addr := range addresses {
444
-		if addr.IsLoopback() {
445
-			return true
446
-		}
447
-	}
448
-	return false
449
-}
450 1
deleted file mode 100644
... ...
@@ -1,340 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"testing"
5
-
6
-	cerrdefs "github.com/containerd/errdefs"
7
-	"gotest.tools/v3/assert"
8
-	is "gotest.tools/v3/assert/cmp"
9
-)
10
-
11
-func TestValidateMirror(t *testing.T) {
12
-	tests := []struct {
13
-		input       string
14
-		output      string
15
-		expectedErr string
16
-	}{
17
-		// Valid cases
18
-		{
19
-			input:  "http://mirror-1.example.com",
20
-			output: "http://mirror-1.example.com/",
21
-		},
22
-		{
23
-			input:  "http://mirror-1.example.com/",
24
-			output: "http://mirror-1.example.com/",
25
-		},
26
-		{
27
-			input:  "https://mirror-1.example.com",
28
-			output: "https://mirror-1.example.com/",
29
-		},
30
-		{
31
-			input:  "https://mirror-1.example.com/",
32
-			output: "https://mirror-1.example.com/",
33
-		},
34
-		{
35
-			input:  "http://localhost",
36
-			output: "http://localhost/",
37
-		},
38
-		{
39
-			input:  "https://localhost",
40
-			output: "https://localhost/",
41
-		},
42
-		{
43
-			input:  "http://localhost:5000",
44
-			output: "http://localhost:5000/",
45
-		},
46
-		{
47
-			input:  "https://localhost:5000",
48
-			output: "https://localhost:5000/",
49
-		},
50
-		{
51
-			input:  "http://127.0.0.1",
52
-			output: "http://127.0.0.1/",
53
-		},
54
-		{
55
-			input:  "https://127.0.0.1",
56
-			output: "https://127.0.0.1/",
57
-		},
58
-		{
59
-			input:  "http://127.0.0.1:5000",
60
-			output: "http://127.0.0.1:5000/",
61
-		},
62
-		{
63
-			input:  "https://127.0.0.1:5000",
64
-			output: "https://127.0.0.1:5000/",
65
-		},
66
-		{
67
-			input:  "http://mirror-1.example.com/v1/",
68
-			output: "http://mirror-1.example.com/v1/",
69
-		},
70
-		{
71
-			input:  "https://mirror-1.example.com/v1/",
72
-			output: "https://mirror-1.example.com/v1/",
73
-		},
74
-
75
-		// Invalid cases
76
-		{
77
-			input:       "!invalid!://%as%",
78
-			expectedErr: `invalid mirror: "!invalid!://%as%" is not a valid URI: parse "!invalid!://%as%": first path segment in URL cannot contain colon`,
79
-		},
80
-		{
81
-			input:       "mirror-1.example.com",
82
-			expectedErr: `invalid mirror: no scheme specified for "mirror-1.example.com": must use either 'https://' or 'http://'`,
83
-		},
84
-		{
85
-			input:       "mirror-1.example.com:5000",
86
-			expectedErr: `invalid mirror: no scheme specified for "mirror-1.example.com:5000": must use either 'https://' or 'http://'`,
87
-		},
88
-		{
89
-			input:       "ftp://mirror-1.example.com",
90
-			expectedErr: `invalid mirror: unsupported scheme "ftp" in "ftp://mirror-1.example.com": must use either 'https://' or 'http://'`,
91
-		},
92
-		{
93
-			input:       "http://mirror-1.example.com/?q=foo",
94
-			expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/?q=foo"`,
95
-		},
96
-		{
97
-			input:       "http://mirror-1.example.com/v1/?q=foo",
98
-			expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/v1/?q=foo"`,
99
-		},
100
-		{
101
-			input:       "http://mirror-1.example.com/v1/?q=foo#frag",
102
-			expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/v1/?q=foo#frag"`,
103
-		},
104
-		{
105
-			input:       "http://mirror-1.example.com?q=foo",
106
-			expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com?q=foo"`,
107
-		},
108
-		{
109
-			input:       "https://mirror-1.example.com#frag",
110
-			expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com#frag"`,
111
-		},
112
-		{
113
-			input:       "https://mirror-1.example.com/#frag",
114
-			expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com/#frag"`,
115
-		},
116
-		{
117
-			input:       "http://foo:bar@mirror-1.example.com/",
118
-			expectedErr: `invalid mirror: username/password not allowed in URI "http://foo:xxxxx@mirror-1.example.com/"`,
119
-		},
120
-		{
121
-			input:       "https://mirror-1.example.com/v1/#frag",
122
-			expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com/v1/#frag"`,
123
-		},
124
-		{
125
-			input:       "https://mirror-1.example.com?q",
126
-			expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com?q"`,
127
-		},
128
-	}
129
-
130
-	for _, tc := range tests {
131
-		t.Run(tc.input, func(t *testing.T) {
132
-			out, err := ValidateMirror(tc.input)
133
-			if tc.expectedErr != "" {
134
-				assert.Error(t, err, tc.expectedErr)
135
-			} else {
136
-				assert.NilError(t, err)
137
-			}
138
-			assert.Check(t, is.Equal(out, tc.output))
139
-		})
140
-	}
141
-}
142
-
143
-func TestLoadInsecureRegistries(t *testing.T) {
144
-	testCases := []struct {
145
-		registries []string
146
-		index      string
147
-		err        string
148
-	}{
149
-		{
150
-			registries: []string{"127.0.0.1"},
151
-			index:      "127.0.0.1",
152
-		},
153
-		{
154
-			registries: []string{"127.0.0.1:8080"},
155
-			index:      "127.0.0.1:8080",
156
-		},
157
-		{
158
-			registries: []string{"2001:db8::1"},
159
-			index:      "2001:db8::1",
160
-		},
161
-		{
162
-			registries: []string{"[2001:db8::1]:80"},
163
-			index:      "[2001:db8::1]:80",
164
-		},
165
-		{
166
-			registries: []string{"http://myregistry.example.com"},
167
-			index:      "myregistry.example.com",
168
-		},
169
-		{
170
-			registries: []string{"https://myregistry.example.com"},
171
-			index:      "myregistry.example.com",
172
-		},
173
-		{
174
-			registries: []string{"HTTP://myregistry.example.com"},
175
-			index:      "myregistry.example.com",
176
-		},
177
-		{
178
-			registries: []string{"svn://myregistry.example.com"},
179
-			err:        "insecure registry svn://myregistry.example.com should not contain '://'",
180
-		},
181
-		{
182
-			registries: []string{"-invalid-registry"},
183
-			err:        "Cannot begin or end with a hyphen",
184
-		},
185
-		{
186
-			registries: []string{`mytest-.com`},
187
-			err:        `insecure registry mytest-.com is not valid: invalid host "mytest-.com"`,
188
-		},
189
-		{
190
-			registries: []string{`1200:0000:AB00:1234:0000:2552:7777:1313:8080`},
191
-			err:        `insecure registry 1200:0000:AB00:1234:0000:2552:7777:1313:8080 is not valid: invalid host "1200:0000:AB00:1234:0000:2552:7777:1313:8080"`,
192
-		},
193
-		{
194
-			registries: []string{`myregistry.example.com:500000`},
195
-			err:        `insecure registry myregistry.example.com:500000 is not valid: invalid port "500000"`,
196
-		},
197
-		{
198
-			registries: []string{`"myregistry.example.com"`},
199
-			err:        `insecure registry "myregistry.example.com" is not valid: invalid host "\"myregistry.example.com\""`,
200
-		},
201
-		{
202
-			registries: []string{`"myregistry.example.com:5000"`},
203
-			err:        `insecure registry "myregistry.example.com:5000" is not valid: invalid host "\"myregistry.example.com"`,
204
-		},
205
-	}
206
-	for _, testCase := range testCases {
207
-		config := &serviceConfig{}
208
-		err := config.loadInsecureRegistries(testCase.registries)
209
-		if testCase.err == "" {
210
-			if err != nil {
211
-				t.Fatalf("expect no error, got '%s'", err)
212
-			}
213
-			match := false
214
-			for index := range config.IndexConfigs {
215
-				if index == testCase.index {
216
-					match = true
217
-				}
218
-			}
219
-			if !match {
220
-				t.Fatalf("expect index configs to contain '%s', got %+v", testCase.index, config.IndexConfigs)
221
-			}
222
-		} else {
223
-			if err == nil {
224
-				t.Fatalf("expect error '%s', got no error", testCase.err)
225
-			}
226
-			assert.ErrorContains(t, err, testCase.err)
227
-			assert.Check(t, cerrdefs.IsInvalidArgument(err))
228
-		}
229
-	}
230
-}
231
-
232
-func TestNewServiceConfig(t *testing.T) {
233
-	tests := []struct {
234
-		doc    string
235
-		opts   ServiceOptions
236
-		errStr string
237
-	}{
238
-		{
239
-			doc: "empty config",
240
-		},
241
-		{
242
-			doc: "invalid mirror",
243
-			opts: ServiceOptions{
244
-				Mirrors: []string{"example.com:5000"},
245
-			},
246
-			errStr: `invalid mirror: no scheme specified for "example.com:5000": must use either 'https://' or 'http://'`,
247
-		},
248
-		{
249
-			doc: "valid mirror",
250
-			opts: ServiceOptions{
251
-				Mirrors: []string{"https://example.com:5000"},
252
-			},
253
-		},
254
-		{
255
-			doc: "invalid insecure registry",
256
-			opts: ServiceOptions{
257
-				InsecureRegistries: []string{"[fe80::]/64"},
258
-			},
259
-			errStr: `insecure registry [fe80::]/64 is not valid: invalid host "[fe80::]/64"`,
260
-		},
261
-		{
262
-			doc: "valid insecure registry",
263
-			opts: ServiceOptions{
264
-				InsecureRegistries: []string{"102.10.8.1/24"},
265
-			},
266
-		},
267
-	}
268
-
269
-	for _, tc := range tests {
270
-		t.Run(tc.doc, func(t *testing.T) {
271
-			_, err := newServiceConfig(tc.opts)
272
-			if tc.errStr != "" {
273
-				assert.Check(t, is.Error(err, tc.errStr))
274
-				assert.Check(t, cerrdefs.IsInvalidArgument(err))
275
-			} else {
276
-				assert.Check(t, err)
277
-			}
278
-		})
279
-	}
280
-}
281
-
282
-func TestValidateIndexName(t *testing.T) {
283
-	valid := []struct {
284
-		index  string
285
-		expect string
286
-	}{
287
-		{
288
-			index:  "index.docker.io",
289
-			expect: "docker.io",
290
-		},
291
-		{
292
-			index:  "example.com",
293
-			expect: "example.com",
294
-		},
295
-		{
296
-			index:  "127.0.0.1:8080",
297
-			expect: "127.0.0.1:8080",
298
-		},
299
-		{
300
-			index:  "mytest-1.com",
301
-			expect: "mytest-1.com",
302
-		},
303
-		{
304
-			index:  "mirror-1.example.com/v1/?q=foo",
305
-			expect: "mirror-1.example.com/v1/?q=foo",
306
-		},
307
-	}
308
-
309
-	for _, testCase := range valid {
310
-		result, err := ValidateIndexName(testCase.index)
311
-		if assert.Check(t, err) {
312
-			assert.Check(t, is.Equal(testCase.expect, result))
313
-		}
314
-	}
315
-}
316
-
317
-func TestValidateIndexNameWithError(t *testing.T) {
318
-	invalid := []struct {
319
-		index string
320
-		err   string
321
-	}{
322
-		{
323
-			index: "docker.io-",
324
-			err:   "invalid index name (docker.io-). Cannot begin or end with a hyphen",
325
-		},
326
-		{
327
-			index: "-example.com",
328
-			err:   "invalid index name (-example.com). Cannot begin or end with a hyphen",
329
-		},
330
-		{
331
-			index: "mirror-1.example.com/v1/?q=foo-",
332
-			err:   "invalid index name (mirror-1.example.com/v1/?q=foo-). Cannot begin or end with a hyphen",
333
-		},
334
-	}
335
-	for _, testCase := range invalid {
336
-		_, err := ValidateIndexName(testCase.index)
337
-		assert.Check(t, is.Error(err, testCase.err))
338
-		assert.Check(t, cerrdefs.IsInvalidArgument(err))
339
-	}
340
-}
341 1
deleted file mode 100644
... ...
@@ -1,67 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"net/url"
5
-
6
-	"github.com/docker/distribution/registry/api/errcode"
7
-	"github.com/pkg/errors"
8
-)
9
-
10
-func translateV2AuthError(err error) error {
11
-	var e *url.Error
12
-	if errors.As(err, &e) {
13
-		var e2 errcode.Error
14
-		if errors.As(e, &e2) && errors.Is(e2.Code, errcode.ErrorCodeUnauthorized) {
15
-			return unauthorizedErr{err}
16
-		}
17
-	}
18
-	return err
19
-}
20
-
21
-func invalidParam(err error) error {
22
-	return invalidParameterErr{err}
23
-}
24
-
25
-func invalidParamf(format string, args ...interface{}) error {
26
-	return invalidParameterErr{errors.Errorf(format, args...)}
27
-}
28
-
29
-func invalidParamWrapf(err error, format string, args ...interface{}) error {
30
-	return invalidParameterErr{errors.Wrapf(err, format, args...)}
31
-}
32
-
33
-type unauthorizedErr struct{ error }
34
-
35
-func (unauthorizedErr) Unauthorized() {}
36
-
37
-func (e unauthorizedErr) Cause() error {
38
-	return e.error
39
-}
40
-
41
-func (e unauthorizedErr) Unwrap() error {
42
-	return e.error
43
-}
44
-
45
-type invalidParameterErr struct{ error }
46
-
47
-func (invalidParameterErr) InvalidParameter() {}
48
-
49
-func (e invalidParameterErr) Unwrap() error {
50
-	return e.error
51
-}
52
-
53
-type systemErr struct{ error }
54
-
55
-func (systemErr) System() {}
56
-
57
-func (e systemErr) Unwrap() error {
58
-	return e.error
59
-}
60
-
61
-type errUnknown struct{ error }
62
-
63
-func (errUnknown) Unknown() {}
64
-
65
-func (e errUnknown) Unwrap() error {
66
-	return e.error
67
-}
68 1
deleted file mode 100644
... ...
@@ -1,155 +0,0 @@
1
-// Package registry contains client primitives to interact with a remote Docker registry.
2
-package registry
3
-
4
-import (
5
-	"context"
6
-	"crypto/tls"
7
-	"net"
8
-	"net/http"
9
-	"os"
10
-	"path/filepath"
11
-	"runtime"
12
-	"strings"
13
-	"time"
14
-
15
-	"github.com/containerd/log"
16
-	"github.com/docker/distribution/registry/client/transport"
17
-	"github.com/docker/go-connections/tlsconfig"
18
-	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
19
-)
20
-
21
-// hostCertsDir returns the config directory for a specific host.
22
-func hostCertsDir(hostnameAndPort string) string {
23
-	if runtime.GOOS == "windows" {
24
-		// Ensure that a directory name is valid; hostnameAndPort may contain
25
-		// a colon (:) if a port is included, and Windows does not allow colons
26
-		// in directory names.
27
-		hostnameAndPort = filepath.FromSlash(strings.ReplaceAll(hostnameAndPort, ":", ""))
28
-	}
29
-	return filepath.Join(CertsDir(), hostnameAndPort)
30
-}
31
-
32
-// newTLSConfig constructs a client TLS configuration based on server defaults
33
-func newTLSConfig(ctx context.Context, hostname string, isSecure bool) (*tls.Config, error) {
34
-	// PreferredServerCipherSuites should have no effect
35
-	tlsConfig := tlsconfig.ServerDefault()
36
-	tlsConfig.InsecureSkipVerify = !isSecure
37
-
38
-	if isSecure {
39
-		hostDir := hostCertsDir(hostname)
40
-		log.G(ctx).Debugf("hostDir: %s", hostDir)
41
-		if err := loadTLSConfig(ctx, hostDir, tlsConfig); err != nil {
42
-			return nil, err
43
-		}
44
-	}
45
-
46
-	return tlsConfig, nil
47
-}
48
-
49
-func hasFile(files []os.DirEntry, name string) bool {
50
-	for _, f := range files {
51
-		if f.Name() == name {
52
-			return true
53
-		}
54
-	}
55
-	return false
56
-}
57
-
58
-// ReadCertsDirectory reads the directory for TLS certificates
59
-// including roots and certificate pairs and updates the
60
-// provided TLS configuration.
61
-func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
62
-	return loadTLSConfig(context.TODO(), directory, tlsConfig)
63
-}
64
-
65
-// loadTLSConfig reads the directory for TLS certificates including roots and
66
-// certificate pairs, and updates the provided TLS configuration.
67
-func loadTLSConfig(ctx context.Context, directory string, tlsConfig *tls.Config) error {
68
-	fs, err := os.ReadDir(directory)
69
-	if err != nil {
70
-		if os.IsNotExist(err) {
71
-			return nil
72
-		}
73
-		return invalidParam(err)
74
-	}
75
-
76
-	for _, f := range fs {
77
-		if ctx.Err() != nil {
78
-			return ctx.Err()
79
-		}
80
-		switch filepath.Ext(f.Name()) {
81
-		case ".crt":
82
-			if tlsConfig.RootCAs == nil {
83
-				systemPool, err := tlsconfig.SystemCertPool()
84
-				if err != nil {
85
-					return invalidParamWrapf(err, "unable to get system cert pool")
86
-				}
87
-				tlsConfig.RootCAs = systemPool
88
-			}
89
-			fileName := filepath.Join(directory, f.Name())
90
-			log.G(ctx).Debugf("crt: %s", fileName)
91
-			data, err := os.ReadFile(fileName)
92
-			if err != nil {
93
-				return err
94
-			}
95
-			tlsConfig.RootCAs.AppendCertsFromPEM(data)
96
-		case ".cert":
97
-			certName := f.Name()
98
-			keyName := certName[:len(certName)-5] + ".key"
99
-			log.G(ctx).Debugf("cert: %s", filepath.Join(directory, certName))
100
-			if !hasFile(fs, keyName) {
101
-				return invalidParamf("missing key %s for client certificate %s. CA certificates must use the extension .crt", keyName, certName)
102
-			}
103
-			cert, err := tls.LoadX509KeyPair(filepath.Join(directory, certName), filepath.Join(directory, keyName))
104
-			if err != nil {
105
-				return err
106
-			}
107
-			tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
108
-		case ".key":
109
-			keyName := f.Name()
110
-			certName := keyName[:len(keyName)-4] + ".cert"
111
-			log.G(ctx).Debugf("key: %s", filepath.Join(directory, keyName))
112
-			if !hasFile(fs, certName) {
113
-				return invalidParamf("missing client certificate %s for key %s", certName, keyName)
114
-			}
115
-		}
116
-	}
117
-
118
-	return nil
119
-}
120
-
121
-// Headers returns request modifiers with a User-Agent and metaHeaders
122
-func Headers(userAgent string, metaHeaders http.Header) []transport.RequestModifier {
123
-	modifiers := []transport.RequestModifier{}
124
-	if userAgent != "" {
125
-		modifiers = append(modifiers, transport.NewHeaderRequestModifier(http.Header{
126
-			"User-Agent": []string{userAgent},
127
-		}))
128
-	}
129
-	if metaHeaders != nil {
130
-		modifiers = append(modifiers, transport.NewHeaderRequestModifier(metaHeaders))
131
-	}
132
-	return modifiers
133
-}
134
-
135
-// newTransport returns a new HTTP transport. If tlsConfig is nil, it uses the
136
-// default TLS configuration.
137
-func newTransport(tlsConfig *tls.Config) http.RoundTripper {
138
-	if tlsConfig == nil {
139
-		tlsConfig = tlsconfig.ServerDefault()
140
-	}
141
-
142
-	return otelhttp.NewTransport(
143
-		&http.Transport{
144
-			Proxy: http.ProxyFromEnvironment,
145
-			DialContext: (&net.Dialer{
146
-				Timeout:   30 * time.Second,
147
-				KeepAlive: 30 * time.Second,
148
-			}).DialContext,
149
-			TLSHandshakeTimeout: 10 * time.Second,
150
-			TLSClientConfig:     tlsConfig,
151
-			// TODO(dmcgowan): Call close idle connections when complete and use keep alive
152
-			DisableKeepAlives: true,
153
-		},
154
-	)
155
-}
156 1
deleted file mode 100644
... ...
@@ -1,120 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"context"
5
-	"encoding/json"
6
-	"io"
7
-	"net/http"
8
-	"net/http/httptest"
9
-	"testing"
10
-
11
-	"github.com/containerd/log"
12
-	"github.com/moby/moby/api/types/registry"
13
-	"gotest.tools/v3/assert"
14
-)
15
-
16
-var (
17
-	testHTTPServer  *httptest.Server
18
-	testHTTPSServer *httptest.Server
19
-)
20
-
21
-func init() {
22
-	r := http.NewServeMux()
23
-
24
-	// /v1/
25
-	r.HandleFunc("/v1/_ping", handlerGetPing)
26
-	r.HandleFunc("/v1/search", handlerSearch)
27
-
28
-	// /v2/
29
-	r.HandleFunc("/v2/version", handlerGetPing)
30
-
31
-	testHTTPServer = httptest.NewServer(handlerAccessLog(r))
32
-	testHTTPSServer = httptest.NewTLSServer(handlerAccessLog(r))
33
-}
34
-
35
-func handlerAccessLog(handler http.Handler) http.Handler {
36
-	logHandler := func(w http.ResponseWriter, r *http.Request) {
37
-		log.G(context.TODO()).Debugf(`%s "%s %s"`, r.RemoteAddr, r.Method, r.URL)
38
-		handler.ServeHTTP(w, r)
39
-	}
40
-	return http.HandlerFunc(logHandler)
41
-}
42
-
43
-func makeURL(req string) string {
44
-	return testHTTPServer.URL + req
45
-}
46
-
47
-func makeHTTPSURL(req string) string {
48
-	return testHTTPSServer.URL + req
49
-}
50
-
51
-func makeIndex(req string) *registry.IndexInfo {
52
-	return &registry.IndexInfo{
53
-		Name: makeURL(req),
54
-	}
55
-}
56
-
57
-func makeHTTPSIndex(req string) *registry.IndexInfo {
58
-	return &registry.IndexInfo{
59
-		Name: makeHTTPSURL(req),
60
-	}
61
-}
62
-
63
-func makePublicIndex() *registry.IndexInfo {
64
-	return &registry.IndexInfo{
65
-		Name:     IndexServer,
66
-		Secure:   true,
67
-		Official: true,
68
-	}
69
-}
70
-
71
-func writeHeaders(w http.ResponseWriter) {
72
-	h := w.Header()
73
-	h.Add("Server", "docker-tests/mock")
74
-	h.Add("Expires", "-1")
75
-	h.Add("Content-Type", "application/json")
76
-	h.Add("Pragma", "no-cache")
77
-	h.Add("Cache-Control", "no-cache")
78
-}
79
-
80
-func writeResponse(w http.ResponseWriter, message interface{}, code int) {
81
-	writeHeaders(w)
82
-	w.WriteHeader(code)
83
-	body, err := json.Marshal(message)
84
-	if err != nil {
85
-		_, _ = io.WriteString(w, err.Error())
86
-		return
87
-	}
88
-	_, _ = w.Write(body)
89
-}
90
-
91
-func handlerGetPing(w http.ResponseWriter, r *http.Request) {
92
-	if r.Method != http.MethodGet {
93
-		writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
94
-		return
95
-	}
96
-	writeResponse(w, true, http.StatusOK)
97
-}
98
-
99
-func handlerSearch(w http.ResponseWriter, r *http.Request) {
100
-	if r.Method != http.MethodGet {
101
-		writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
102
-		return
103
-	}
104
-	result := &registry.SearchResults{
105
-		Query:      "fakequery",
106
-		NumResults: 1,
107
-		Results:    []registry.SearchResult{{Name: "fakeimage", StarCount: 42}},
108
-	}
109
-	writeResponse(w, result, http.StatusOK)
110
-}
111
-
112
-func TestPing(t *testing.T) {
113
-	res, err := http.Get(makeURL("/v1/_ping"))
114
-	if err != nil {
115
-		t.Fatal(err)
116
-	}
117
-	assert.Equal(t, res.StatusCode, http.StatusOK, "")
118
-	assert.Equal(t, res.Header.Get("Server"), "docker-tests/mock")
119
-	_ = res.Body.Close()
120
-}
121 1
deleted file mode 100644
... ...
@@ -1,637 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"errors"
5
-	"net"
6
-	"testing"
7
-
8
-	"github.com/distribution/reference"
9
-	"github.com/moby/moby/api/types/registry"
10
-	"gotest.tools/v3/assert"
11
-	is "gotest.tools/v3/assert/cmp"
12
-)
13
-
14
-// overrideLookupIP overrides net.LookupIP for testing.
15
-func overrideLookupIP(t *testing.T) {
16
-	t.Helper()
17
-	restoreLookup := lookupIP
18
-
19
-	// override net.LookupIP
20
-	lookupIP = func(host string) ([]net.IP, error) {
21
-		mockHosts := map[string][]net.IP{
22
-			"":            {net.ParseIP("0.0.0.0")},
23
-			"localhost":   {net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
24
-			"example.com": {net.ParseIP("42.42.42.42")},
25
-			"other.com":   {net.ParseIP("43.43.43.43")},
26
-		}
27
-		if addrs, ok := mockHosts[host]; ok {
28
-			return addrs, nil
29
-		}
30
-		return nil, errors.New("lookup: no such host")
31
-	}
32
-	t.Cleanup(func() {
33
-		lookupIP = restoreLookup
34
-	})
35
-}
36
-
37
-func TestParseRepositoryInfo(t *testing.T) {
38
-	type staticRepositoryInfo struct {
39
-		Index         *registry.IndexInfo
40
-		RemoteName    string
41
-		CanonicalName string
42
-		LocalName     string
43
-	}
44
-
45
-	tests := map[string]staticRepositoryInfo{
46
-		"fooo/bar": {
47
-			Index: &registry.IndexInfo{
48
-				Name:     IndexName,
49
-				Mirrors:  []string{},
50
-				Official: true,
51
-				Secure:   true,
52
-			},
53
-			RemoteName:    "fooo/bar",
54
-			LocalName:     "fooo/bar",
55
-			CanonicalName: "docker.io/fooo/bar",
56
-		},
57
-		"library/ubuntu": {
58
-			Index: &registry.IndexInfo{
59
-				Name:     IndexName,
60
-				Mirrors:  []string{},
61
-				Official: true,
62
-				Secure:   true,
63
-			},
64
-			RemoteName:    "library/ubuntu",
65
-			LocalName:     "ubuntu",
66
-			CanonicalName: "docker.io/library/ubuntu",
67
-		},
68
-		"nonlibrary/ubuntu": {
69
-			Index: &registry.IndexInfo{
70
-				Name:     IndexName,
71
-				Mirrors:  []string{},
72
-				Official: true,
73
-				Secure:   true,
74
-			},
75
-			RemoteName:    "nonlibrary/ubuntu",
76
-			LocalName:     "nonlibrary/ubuntu",
77
-			CanonicalName: "docker.io/nonlibrary/ubuntu",
78
-		},
79
-		"ubuntu": {
80
-			Index: &registry.IndexInfo{
81
-				Name:     IndexName,
82
-				Mirrors:  []string{},
83
-				Official: true,
84
-				Secure:   true,
85
-			},
86
-			RemoteName:    "library/ubuntu",
87
-			LocalName:     "ubuntu",
88
-			CanonicalName: "docker.io/library/ubuntu",
89
-		},
90
-		"other/library": {
91
-			Index: &registry.IndexInfo{
92
-				Name:     IndexName,
93
-				Mirrors:  []string{},
94
-				Official: true,
95
-				Secure:   true,
96
-			},
97
-			RemoteName:    "other/library",
98
-			LocalName:     "other/library",
99
-			CanonicalName: "docker.io/other/library",
100
-		},
101
-		"127.0.0.1:8000/private/moonbase": {
102
-			Index: &registry.IndexInfo{
103
-				Name:     "127.0.0.1:8000",
104
-				Mirrors:  []string{},
105
-				Official: false,
106
-				Secure:   false,
107
-			},
108
-			RemoteName:    "private/moonbase",
109
-			LocalName:     "127.0.0.1:8000/private/moonbase",
110
-			CanonicalName: "127.0.0.1:8000/private/moonbase",
111
-		},
112
-		"127.0.0.1:8000/privatebase": {
113
-			Index: &registry.IndexInfo{
114
-				Name:     "127.0.0.1:8000",
115
-				Mirrors:  []string{},
116
-				Official: false,
117
-				Secure:   false,
118
-			},
119
-			RemoteName:    "privatebase",
120
-			LocalName:     "127.0.0.1:8000/privatebase",
121
-			CanonicalName: "127.0.0.1:8000/privatebase",
122
-		},
123
-		"[::1]:8000/private/moonbase": {
124
-			Index: &registry.IndexInfo{
125
-				Name:     "[::1]:8000",
126
-				Mirrors:  []string{},
127
-				Official: false,
128
-				Secure:   false,
129
-			},
130
-			RemoteName:    "private/moonbase",
131
-			LocalName:     "[::1]:8000/private/moonbase",
132
-			CanonicalName: "[::1]:8000/private/moonbase",
133
-		},
134
-		"[::1]:8000/privatebase": {
135
-			Index: &registry.IndexInfo{
136
-				Name:     "[::1]:8000",
137
-				Mirrors:  []string{},
138
-				Official: false,
139
-				Secure:   false,
140
-			},
141
-			RemoteName:    "privatebase",
142
-			LocalName:     "[::1]:8000/privatebase",
143
-			CanonicalName: "[::1]:8000/privatebase",
144
-		},
145
-		// IPv6 only has a single loopback address, so ::2 is not a loopback,
146
-		// hence not marked "insecure".
147
-		"[::2]:8000/private/moonbase": {
148
-			Index: &registry.IndexInfo{
149
-				Name:     "[::2]:8000",
150
-				Mirrors:  []string{},
151
-				Official: false,
152
-				Secure:   true,
153
-			},
154
-			RemoteName:    "private/moonbase",
155
-			LocalName:     "[::2]:8000/private/moonbase",
156
-			CanonicalName: "[::2]:8000/private/moonbase",
157
-		},
158
-		// IPv6 only has a single loopback address, so ::2 is not a loopback,
159
-		// hence not marked "insecure".
160
-		"[::2]:8000/privatebase": {
161
-			Index: &registry.IndexInfo{
162
-				Name:     "[::2]:8000",
163
-				Mirrors:  []string{},
164
-				Official: false,
165
-				Secure:   true,
166
-			},
167
-			RemoteName:    "privatebase",
168
-			LocalName:     "[::2]:8000/privatebase",
169
-			CanonicalName: "[::2]:8000/privatebase",
170
-		},
171
-		"localhost:8000/private/moonbase": {
172
-			Index: &registry.IndexInfo{
173
-				Name:     "localhost:8000",
174
-				Mirrors:  []string{},
175
-				Official: false,
176
-				Secure:   false,
177
-			},
178
-			RemoteName:    "private/moonbase",
179
-			LocalName:     "localhost:8000/private/moonbase",
180
-			CanonicalName: "localhost:8000/private/moonbase",
181
-		},
182
-		"localhost:8000/privatebase": {
183
-			Index: &registry.IndexInfo{
184
-				Name:     "localhost:8000",
185
-				Mirrors:  []string{},
186
-				Official: false,
187
-				Secure:   false,
188
-			},
189
-			RemoteName:    "privatebase",
190
-			LocalName:     "localhost:8000/privatebase",
191
-			CanonicalName: "localhost:8000/privatebase",
192
-		},
193
-		"example.com/private/moonbase": {
194
-			Index: &registry.IndexInfo{
195
-				Name:     "example.com",
196
-				Mirrors:  []string{},
197
-				Official: false,
198
-				Secure:   true,
199
-			},
200
-			RemoteName:    "private/moonbase",
201
-			LocalName:     "example.com/private/moonbase",
202
-			CanonicalName: "example.com/private/moonbase",
203
-		},
204
-		"example.com/privatebase": {
205
-			Index: &registry.IndexInfo{
206
-				Name:     "example.com",
207
-				Mirrors:  []string{},
208
-				Official: false,
209
-				Secure:   true,
210
-			},
211
-			RemoteName:    "privatebase",
212
-			LocalName:     "example.com/privatebase",
213
-			CanonicalName: "example.com/privatebase",
214
-		},
215
-		"example.com:8000/private/moonbase": {
216
-			Index: &registry.IndexInfo{
217
-				Name:     "example.com:8000",
218
-				Mirrors:  []string{},
219
-				Official: false,
220
-				Secure:   true,
221
-			},
222
-			RemoteName:    "private/moonbase",
223
-			LocalName:     "example.com:8000/private/moonbase",
224
-			CanonicalName: "example.com:8000/private/moonbase",
225
-		},
226
-		"example.com:8000/privatebase": {
227
-			Index: &registry.IndexInfo{
228
-				Name:     "example.com:8000",
229
-				Mirrors:  []string{},
230
-				Official: false,
231
-				Secure:   true,
232
-			},
233
-			RemoteName:    "privatebase",
234
-			LocalName:     "example.com:8000/privatebase",
235
-			CanonicalName: "example.com:8000/privatebase",
236
-		},
237
-		"localhost/private/moonbase": {
238
-			Index: &registry.IndexInfo{
239
-				Name:     "localhost",
240
-				Mirrors:  []string{},
241
-				Official: false,
242
-				Secure:   false,
243
-			},
244
-			RemoteName:    "private/moonbase",
245
-			LocalName:     "localhost/private/moonbase",
246
-			CanonicalName: "localhost/private/moonbase",
247
-		},
248
-		"localhost/privatebase": {
249
-			Index: &registry.IndexInfo{
250
-				Name:     "localhost",
251
-				Mirrors:  []string{},
252
-				Official: false,
253
-				Secure:   false,
254
-			},
255
-			RemoteName:    "privatebase",
256
-			LocalName:     "localhost/privatebase",
257
-			CanonicalName: "localhost/privatebase",
258
-		},
259
-		IndexName + "/public/moonbase": {
260
-			Index: &registry.IndexInfo{
261
-				Name:     IndexName,
262
-				Mirrors:  []string{},
263
-				Official: true,
264
-				Secure:   true,
265
-			},
266
-			RemoteName:    "public/moonbase",
267
-			LocalName:     "public/moonbase",
268
-			CanonicalName: "docker.io/public/moonbase",
269
-		},
270
-		"index." + IndexName + "/public/moonbase": {
271
-			Index: &registry.IndexInfo{
272
-				Name:     IndexName,
273
-				Mirrors:  []string{},
274
-				Official: true,
275
-				Secure:   true,
276
-			},
277
-			RemoteName:    "public/moonbase",
278
-			LocalName:     "public/moonbase",
279
-			CanonicalName: "docker.io/public/moonbase",
280
-		},
281
-		"ubuntu-12.04-base": {
282
-			Index: &registry.IndexInfo{
283
-				Name:     IndexName,
284
-				Mirrors:  []string{},
285
-				Official: true,
286
-				Secure:   true,
287
-			},
288
-			RemoteName:    "library/ubuntu-12.04-base",
289
-			LocalName:     "ubuntu-12.04-base",
290
-			CanonicalName: "docker.io/library/ubuntu-12.04-base",
291
-		},
292
-		IndexName + "/ubuntu-12.04-base": {
293
-			Index: &registry.IndexInfo{
294
-				Name:     IndexName,
295
-				Mirrors:  []string{},
296
-				Official: true,
297
-				Secure:   true,
298
-			},
299
-			RemoteName:    "library/ubuntu-12.04-base",
300
-			LocalName:     "ubuntu-12.04-base",
301
-			CanonicalName: "docker.io/library/ubuntu-12.04-base",
302
-		},
303
-		"index." + IndexName + "/ubuntu-12.04-base": {
304
-			Index: &registry.IndexInfo{
305
-				Name:     IndexName,
306
-				Mirrors:  []string{},
307
-				Official: true,
308
-				Secure:   true,
309
-			},
310
-			RemoteName:    "library/ubuntu-12.04-base",
311
-			LocalName:     "ubuntu-12.04-base",
312
-			CanonicalName: "docker.io/library/ubuntu-12.04-base",
313
-		},
314
-	}
315
-
316
-	for reposName, expected := range tests {
317
-		t.Run(reposName, func(t *testing.T) {
318
-			named, err := reference.ParseNormalizedNamed(reposName)
319
-			assert.NilError(t, err)
320
-
321
-			repoInfo, err := ParseRepositoryInfo(named)
322
-			assert.NilError(t, err)
323
-
324
-			assert.Check(t, is.DeepEqual(repoInfo.Index, expected.Index))
325
-			assert.Check(t, is.Equal(reference.Path(repoInfo.Name), expected.RemoteName))
326
-			assert.Check(t, is.Equal(reference.FamiliarName(repoInfo.Name), expected.LocalName))
327
-			assert.Check(t, is.Equal(repoInfo.Name.Name(), expected.CanonicalName))
328
-		})
329
-	}
330
-}
331
-
332
-func TestNewIndexInfo(t *testing.T) {
333
-	overrideLookupIP(t)
334
-
335
-	// ipv6Loopback is the CIDR for the IPv6 loopback address ("::1"); "::1/128"
336
-	ipv6Loopback := &net.IPNet{
337
-		IP:   net.IPv6loopback,
338
-		Mask: net.CIDRMask(128, 128),
339
-	}
340
-
341
-	// ipv4Loopback is the CIDR for IPv4 loopback addresses ("127.0.0.0/8")
342
-	ipv4Loopback := &net.IPNet{
343
-		IP:   net.IPv4(127, 0, 0, 0),
344
-		Mask: net.CIDRMask(8, 32),
345
-	}
346
-
347
-	// emptyServiceConfig is a default service-config for situations where
348
-	// no config-file is available (e.g. when used in the CLI). It won't
349
-	// have mirrors configured, but does have the default insecure registry
350
-	// CIDRs for loopback interfaces configured.
351
-	emptyServiceConfig := &serviceConfig{
352
-		IndexConfigs: map[string]*registry.IndexInfo{
353
-			IndexName: {
354
-				Name:     IndexName,
355
-				Mirrors:  []string{},
356
-				Secure:   true,
357
-				Official: true,
358
-			},
359
-		},
360
-		InsecureRegistryCIDRs: []*registry.NetIPNet{
361
-			(*registry.NetIPNet)(ipv6Loopback),
362
-			(*registry.NetIPNet)(ipv4Loopback),
363
-		},
364
-	}
365
-
366
-	expectedIndexInfos := map[string]*registry.IndexInfo{
367
-		IndexName: {
368
-			Name:     IndexName,
369
-			Official: true,
370
-			Secure:   true,
371
-			Mirrors:  []string{},
372
-		},
373
-		"index." + IndexName: {
374
-			Name:     IndexName,
375
-			Official: true,
376
-			Secure:   true,
377
-			Mirrors:  []string{},
378
-		},
379
-		"example.com": {
380
-			Name:     "example.com",
381
-			Official: false,
382
-			Secure:   true,
383
-			Mirrors:  []string{},
384
-		},
385
-		"127.0.0.1:5000": {
386
-			Name:     "127.0.0.1:5000",
387
-			Official: false,
388
-			Secure:   false,
389
-			Mirrors:  []string{},
390
-		},
391
-	}
392
-	t.Run("no mirrors", func(t *testing.T) {
393
-		for indexName, expected := range expectedIndexInfos {
394
-			t.Run(indexName, func(t *testing.T) {
395
-				actual := newIndexInfo(emptyServiceConfig, indexName)
396
-				assert.Check(t, is.DeepEqual(actual, expected))
397
-			})
398
-		}
399
-	})
400
-
401
-	expectedIndexInfos = map[string]*registry.IndexInfo{
402
-		IndexName: {
403
-			Name:     IndexName,
404
-			Official: true,
405
-			Secure:   true,
406
-			Mirrors:  []string{"http://mirror1.local/", "http://mirror2.local/"},
407
-		},
408
-		"index." + IndexName: {
409
-			Name:     IndexName,
410
-			Official: true,
411
-			Secure:   true,
412
-			Mirrors:  []string{"http://mirror1.local/", "http://mirror2.local/"},
413
-		},
414
-		"example.com": {
415
-			Name:     "example.com",
416
-			Official: false,
417
-			Secure:   false,
418
-			Mirrors:  []string{},
419
-		},
420
-		"example.com:5000": {
421
-			Name:     "example.com:5000",
422
-			Official: false,
423
-			Secure:   true,
424
-			Mirrors:  []string{},
425
-		},
426
-		"127.0.0.1": {
427
-			Name:     "127.0.0.1",
428
-			Official: false,
429
-			Secure:   false,
430
-			Mirrors:  []string{},
431
-		},
432
-		"127.0.0.1:5000": {
433
-			Name:     "127.0.0.1:5000",
434
-			Official: false,
435
-			Secure:   false,
436
-			Mirrors:  []string{},
437
-		},
438
-		"127.255.255.255": {
439
-			Name:     "127.255.255.255",
440
-			Official: false,
441
-			Secure:   false,
442
-			Mirrors:  []string{},
443
-		},
444
-		"127.255.255.255:5000": {
445
-			Name:     "127.255.255.255:5000",
446
-			Official: false,
447
-			Secure:   false,
448
-			Mirrors:  []string{},
449
-		},
450
-		"::1": {
451
-			Name:     "::1",
452
-			Official: false,
453
-			Secure:   false,
454
-			Mirrors:  []string{},
455
-		},
456
-		"[::1]:5000": {
457
-			Name:     "[::1]:5000",
458
-			Official: false,
459
-			Secure:   false,
460
-			Mirrors:  []string{},
461
-		},
462
-		// IPv6 only has a single loopback address, so ::2 is not a loopback,
463
-		// hence not marked "insecure".
464
-		"::2": {
465
-			Name:     "::2",
466
-			Official: false,
467
-			Secure:   true,
468
-			Mirrors:  []string{},
469
-		},
470
-		// IPv6 only has a single loopback address, so ::2 is not a loopback,
471
-		// hence not marked "insecure".
472
-		"[::2]:5000": {
473
-			Name:     "[::2]:5000",
474
-			Official: false,
475
-			Secure:   true,
476
-			Mirrors:  []string{},
477
-		},
478
-		"other.com": {
479
-			Name:     "other.com",
480
-			Official: false,
481
-			Secure:   true,
482
-			Mirrors:  []string{},
483
-		},
484
-	}
485
-	t.Run("mirrors", func(t *testing.T) {
486
-		// Note that newServiceConfig calls ValidateMirror internally, which normalizes
487
-		// mirror-URLs to have a trailing slash.
488
-		config, err := newServiceConfig(ServiceOptions{
489
-			Mirrors:            []string{"http://mirror1.local", "http://mirror2.local"},
490
-			InsecureRegistries: []string{"example.com"},
491
-		})
492
-		assert.NilError(t, err)
493
-		for indexName, expected := range expectedIndexInfos {
494
-			t.Run(indexName, func(t *testing.T) {
495
-				actual := newIndexInfo(config, indexName)
496
-				assert.Check(t, is.DeepEqual(actual, expected))
497
-			})
498
-		}
499
-	})
500
-
501
-	expectedIndexInfos = map[string]*registry.IndexInfo{
502
-		"example.com": {
503
-			Name:     "example.com",
504
-			Official: false,
505
-			Secure:   false,
506
-			Mirrors:  []string{},
507
-		},
508
-		"example.com:5000": {
509
-			Name:     "example.com:5000",
510
-			Official: false,
511
-			Secure:   false,
512
-			Mirrors:  []string{},
513
-		},
514
-		"127.0.0.1": {
515
-			Name:     "127.0.0.1",
516
-			Official: false,
517
-			Secure:   false,
518
-			Mirrors:  []string{},
519
-		},
520
-		"127.0.0.1:5000": {
521
-			Name:     "127.0.0.1:5000",
522
-			Official: false,
523
-			Secure:   false,
524
-			Mirrors:  []string{},
525
-		},
526
-		"42.42.0.1:5000": {
527
-			Name:     "42.42.0.1:5000",
528
-			Official: false,
529
-			Secure:   false,
530
-			Mirrors:  []string{},
531
-		},
532
-		"42.43.0.1:5000": {
533
-			Name:     "42.43.0.1:5000",
534
-			Official: false,
535
-			Secure:   true,
536
-			Mirrors:  []string{},
537
-		},
538
-		"other.com": {
539
-			Name:     "other.com",
540
-			Official: false,
541
-			Secure:   true,
542
-			Mirrors:  []string{},
543
-		},
544
-	}
545
-	t.Run("custom insecure", func(t *testing.T) {
546
-		config, err := newServiceConfig(ServiceOptions{
547
-			InsecureRegistries: []string{"42.42.0.0/16"},
548
-		})
549
-		assert.NilError(t, err)
550
-		for indexName, expected := range expectedIndexInfos {
551
-			t.Run(indexName, func(t *testing.T) {
552
-				actual := newIndexInfo(config, indexName)
553
-				assert.Check(t, is.DeepEqual(actual, expected))
554
-			})
555
-		}
556
-	})
557
-}
558
-
559
-func TestMirrorEndpointLookup(t *testing.T) {
560
-	containsMirror := func(endpoints []APIEndpoint) bool {
561
-		for _, pe := range endpoints {
562
-			if pe.URL.Host == "my.mirror" {
563
-				return true
564
-			}
565
-		}
566
-		return false
567
-	}
568
-	cfg, err := newServiceConfig(ServiceOptions{
569
-		Mirrors: []string{"https://my.mirror"},
570
-	})
571
-	assert.NilError(t, err)
572
-	s := Service{config: cfg}
573
-
574
-	imageName, err := reference.WithName(IndexName + "/test/image")
575
-	if err != nil {
576
-		t.Error(err)
577
-	}
578
-	pushAPIEndpoints, err := s.LookupPushEndpoints(reference.Domain(imageName))
579
-	if err != nil {
580
-		t.Fatal(err)
581
-	}
582
-	if containsMirror(pushAPIEndpoints) {
583
-		t.Fatal("Push endpoint should not contain mirror")
584
-	}
585
-
586
-	pullAPIEndpoints, err := s.LookupPullEndpoints(reference.Domain(imageName))
587
-	if err != nil {
588
-		t.Fatal(err)
589
-	}
590
-	if !containsMirror(pullAPIEndpoints) {
591
-		t.Fatal("Pull endpoint should contain mirror")
592
-	}
593
-}
594
-
595
-func TestIsSecureIndex(t *testing.T) {
596
-	overrideLookupIP(t)
597
-	tests := []struct {
598
-		addr               string
599
-		insecureRegistries []string
600
-		expected           bool
601
-	}{
602
-		{IndexName, nil, true},
603
-		{"example.com", []string{}, true},
604
-		{"example.com", []string{"example.com"}, false},
605
-		{"localhost", []string{"localhost:5000"}, false},
606
-		{"localhost:5000", []string{"localhost:5000"}, false},
607
-		{"localhost", []string{"example.com"}, false},
608
-		{"127.0.0.1:5000", []string{"127.0.0.1:5000"}, false},
609
-		{"localhost", nil, false},
610
-		{"localhost:5000", nil, false},
611
-		{"127.0.0.1", nil, false},
612
-		{"localhost", []string{"example.com"}, false},
613
-		{"127.0.0.1", []string{"example.com"}, false},
614
-		{"example.com", nil, true},
615
-		{"example.com", []string{"example.com"}, false},
616
-		{"127.0.0.1", []string{"example.com"}, false},
617
-		{"127.0.0.1:5000", []string{"example.com"}, false},
618
-		{"example.com:5000", []string{"42.42.0.0/16"}, false},
619
-		{"example.com", []string{"42.42.0.0/16"}, false},
620
-		{"example.com:5000", []string{"42.42.42.42/8"}, false},
621
-		{"127.0.0.1:5000", []string{"127.0.0.0/8"}, false},
622
-		{"42.42.42.42:5000", []string{"42.1.1.1/8"}, false},
623
-		{"invalid.example.com", []string{"42.42.0.0/16"}, true},
624
-		{"invalid.example.com", []string{"invalid.example.com"}, false},
625
-		{"invalid.example.com:5000", []string{"invalid.example.com"}, true},
626
-		{"invalid.example.com:5000", []string{"invalid.example.com:5000"}, false},
627
-	}
628
-	for _, tc := range tests {
629
-		config, err := newServiceConfig(ServiceOptions{
630
-			InsecureRegistries: tc.insecureRegistries,
631
-		})
632
-		assert.NilError(t, err)
633
-
634
-		sec := config.isSecureIndex(tc.addr)
635
-		assert.Equal(t, sec, tc.expected, "isSecureIndex failed for %q %v, expected %v got %v", tc.addr, tc.insecureRegistries, tc.expected, sec)
636
-	}
637
-}
638 1
deleted file mode 100644
... ...
@@ -1,100 +0,0 @@
1
-package resumable
2
-
3
-import (
4
-	"context"
5
-	"errors"
6
-	"fmt"
7
-	"io"
8
-	"net/http"
9
-	"time"
10
-
11
-	"github.com/containerd/log"
12
-)
13
-
14
-type requestReader struct {
15
-	client          *http.Client
16
-	request         *http.Request
17
-	lastRange       int64
18
-	totalSize       int64
19
-	currentResponse *http.Response
20
-	failures        uint32
21
-	maxFailures     uint32
22
-	waitDuration    time.Duration
23
-}
24
-
25
-// NewRequestReader makes it possible to resume reading a request's body transparently
26
-// maxfail is the number of times we retry to make requests again (not resumes)
27
-// totalsize is the total length of the body; auto detect if not provided
28
-func NewRequestReader(c *http.Client, r *http.Request, maxfail uint32, totalsize int64) io.ReadCloser {
29
-	return &requestReader{client: c, request: r, maxFailures: maxfail, totalSize: totalsize, waitDuration: 5 * time.Second}
30
-}
31
-
32
-// NewRequestReaderWithInitialResponse makes it possible to resume
33
-// reading the body of an already initiated request.
34
-func NewRequestReaderWithInitialResponse(c *http.Client, r *http.Request, maxfail uint32, totalsize int64, initialResponse *http.Response) io.ReadCloser {
35
-	return &requestReader{client: c, request: r, maxFailures: maxfail, totalSize: totalsize, currentResponse: initialResponse, waitDuration: 5 * time.Second}
36
-}
37
-
38
-func (r *requestReader) Read(p []byte) (n int, _ error) {
39
-	if r.client == nil || r.request == nil {
40
-		return 0, errors.New("client and request can't be nil")
41
-	}
42
-
43
-	var err error
44
-	isFreshRequest := false
45
-	if r.lastRange != 0 && r.currentResponse == nil {
46
-		readRange := fmt.Sprintf("bytes=%d-%d", r.lastRange, r.totalSize)
47
-		r.request.Header.Set("Range", readRange)
48
-		time.Sleep(r.waitDuration)
49
-	}
50
-	if r.currentResponse == nil {
51
-		r.currentResponse, err = r.client.Do(r.request)
52
-		isFreshRequest = true
53
-	}
54
-	if err != nil && r.failures+1 != r.maxFailures {
55
-		r.cleanUpResponse()
56
-		r.failures++
57
-		time.Sleep(time.Duration(r.failures) * r.waitDuration)
58
-		return 0, nil
59
-	} else if err != nil {
60
-		r.cleanUpResponse()
61
-		return 0, err
62
-	}
63
-	if r.currentResponse.StatusCode == http.StatusRequestedRangeNotSatisfiable && r.lastRange == r.totalSize && r.currentResponse.ContentLength == 0 {
64
-		r.cleanUpResponse()
65
-		return 0, io.EOF
66
-	} else if r.currentResponse.StatusCode != http.StatusPartialContent && r.lastRange != 0 && isFreshRequest {
67
-		r.cleanUpResponse()
68
-		return 0, errors.New("the server doesn't support byte ranges")
69
-	}
70
-	if r.totalSize == 0 {
71
-		r.totalSize = r.currentResponse.ContentLength
72
-	} else if r.totalSize <= 0 {
73
-		r.cleanUpResponse()
74
-		return 0, errors.New("failed to auto detect content length")
75
-	}
76
-	n, err = r.currentResponse.Body.Read(p)
77
-	r.lastRange += int64(n)
78
-	if err != nil {
79
-		r.cleanUpResponse()
80
-	}
81
-	if err != nil && !errors.Is(err, io.EOF) {
82
-		log.G(context.TODO()).Infof("encountered error during pull and clearing it before resume: %s", err)
83
-		err = nil
84
-	}
85
-	return n, err
86
-}
87
-
88
-func (r *requestReader) Close() error {
89
-	r.cleanUpResponse()
90
-	r.client = nil
91
-	r.request = nil
92
-	return nil
93
-}
94
-
95
-func (r *requestReader) cleanUpResponse() {
96
-	if r.currentResponse != nil {
97
-		r.currentResponse.Body.Close()
98
-		r.currentResponse = nil
99
-	}
100
-}
101 1
deleted file mode 100644
... ...
@@ -1,258 +0,0 @@
1
-package resumable
2
-
3
-import (
4
-	"errors"
5
-	"fmt"
6
-	"io"
7
-	"net/http"
8
-	"net/http/httptest"
9
-	"strings"
10
-	"testing"
11
-	"time"
12
-
13
-	"gotest.tools/v3/assert"
14
-	is "gotest.tools/v3/assert/cmp"
15
-)
16
-
17
-func TestResumableRequestHeaderSimpleErrors(t *testing.T) {
18
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19
-		fmt.Fprintln(w, "Hello, world !")
20
-	}))
21
-	defer ts.Close()
22
-
23
-	client := &http.Client{}
24
-
25
-	var req *http.Request
26
-	req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody)
27
-	assert.NilError(t, err)
28
-
29
-	resreq := &requestReader{}
30
-	_, err = resreq.Read([]byte{})
31
-	assert.Check(t, is.Error(err, "client and request can't be nil"))
32
-
33
-	resreq = &requestReader{
34
-		client:    client,
35
-		request:   req,
36
-		totalSize: -1,
37
-	}
38
-	_, err = resreq.Read([]byte{})
39
-	assert.Check(t, is.Error(err, "failed to auto detect content length"))
40
-}
41
-
42
-// Not too much failures, bails out after some wait
43
-func TestResumableRequestHeaderNotTooMuchFailures(t *testing.T) {
44
-	client := &http.Client{}
45
-
46
-	var badReq *http.Request
47
-	badReq, err := http.NewRequest(http.MethodGet, "I'm not an url", http.NoBody)
48
-	assert.NilError(t, err)
49
-
50
-	resreq := &requestReader{
51
-		client:       client,
52
-		request:      badReq,
53
-		failures:     0,
54
-		maxFailures:  2,
55
-		waitDuration: 10 * time.Millisecond,
56
-	}
57
-	read, err := resreq.Read([]byte{})
58
-	assert.NilError(t, err)
59
-	assert.Check(t, is.Equal(0, read))
60
-}
61
-
62
-// Too much failures, returns the error
63
-func TestResumableRequestHeaderTooMuchFailures(t *testing.T) {
64
-	client := &http.Client{}
65
-
66
-	var badReq *http.Request
67
-	badReq, err := http.NewRequest(http.MethodGet, "I'm not an url", http.NoBody)
68
-	assert.NilError(t, err)
69
-
70
-	resreq := &requestReader{
71
-		client:      client,
72
-		request:     badReq,
73
-		failures:    0,
74
-		maxFailures: 1,
75
-	}
76
-	defer resreq.Close()
77
-
78
-	read, err := resreq.Read([]byte{})
79
-	assert.Assert(t, err != nil)
80
-	assert.Check(t, is.ErrorContains(err, "unsupported protocol scheme"))
81
-	assert.Check(t, is.ErrorContains(err, "I%27m%20not%20an%20url"))
82
-	assert.Check(t, is.Equal(0, read))
83
-}
84
-
85
-type errorReaderCloser struct{}
86
-
87
-func (errorReaderCloser) Close() error { return nil }
88
-
89
-func (errorReaderCloser) Read(p []byte) (int, error) {
90
-	return 0, errors.New("an error occurred")
91
-}
92
-
93
-// If an unknown error is encountered, return 0, nil and log it
94
-func TestResumableRequestReaderWithReadError(t *testing.T) {
95
-	var req *http.Request
96
-	req, err := http.NewRequest(http.MethodGet, "", http.NoBody)
97
-	assert.NilError(t, err)
98
-
99
-	client := &http.Client{}
100
-
101
-	response := &http.Response{
102
-		Status:        "500 Internal Server",
103
-		StatusCode:    http.StatusInternalServerError,
104
-		ContentLength: 0,
105
-		Close:         true,
106
-		Body:          errorReaderCloser{},
107
-	}
108
-
109
-	resreq := &requestReader{
110
-		client:          client,
111
-		request:         req,
112
-		currentResponse: response,
113
-		lastRange:       1,
114
-		totalSize:       1,
115
-	}
116
-	defer resreq.Close()
117
-
118
-	buf := make([]byte, 1)
119
-	read, err := resreq.Read(buf)
120
-	assert.NilError(t, err)
121
-
122
-	assert.Check(t, is.Equal(0, read))
123
-}
124
-
125
-func TestResumableRequestReaderWithEOFWith416Response(t *testing.T) {
126
-	var req *http.Request
127
-	req, err := http.NewRequest(http.MethodGet, "", http.NoBody)
128
-	assert.NilError(t, err)
129
-
130
-	client := &http.Client{}
131
-
132
-	response := &http.Response{
133
-		Status:        "416 Requested Range Not Satisfiable",
134
-		StatusCode:    http.StatusRequestedRangeNotSatisfiable,
135
-		ContentLength: 0,
136
-		Close:         true,
137
-		Body:          io.NopCloser(strings.NewReader("")),
138
-	}
139
-
140
-	resreq := &requestReader{
141
-		client:          client,
142
-		request:         req,
143
-		currentResponse: response,
144
-		lastRange:       1,
145
-		totalSize:       1,
146
-	}
147
-	defer resreq.Close()
148
-
149
-	buf := make([]byte, 1)
150
-	_, err = resreq.Read(buf)
151
-	assert.Check(t, is.Error(err, io.EOF.Error()))
152
-}
153
-
154
-func TestResumableRequestReaderWithServerDoesntSupportByteRanges(t *testing.T) {
155
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
156
-		if r.Header.Get("Range") == "" {
157
-			t.Fatalf("Expected a Range HTTP header, got nothing")
158
-		}
159
-	}))
160
-	defer ts.Close()
161
-
162
-	var req *http.Request
163
-	req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody)
164
-	assert.NilError(t, err)
165
-
166
-	client := &http.Client{}
167
-
168
-	resreq := &requestReader{
169
-		client:    client,
170
-		request:   req,
171
-		lastRange: 1,
172
-	}
173
-	defer resreq.Close()
174
-
175
-	buf := make([]byte, 2)
176
-	_, err = resreq.Read(buf)
177
-	assert.Check(t, is.Error(err, "the server doesn't support byte ranges"))
178
-}
179
-
180
-func TestResumableRequestReaderWithZeroTotalSize(t *testing.T) {
181
-	srvtxt := "some response text data"
182
-
183
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
184
-		fmt.Fprintln(w, srvtxt)
185
-	}))
186
-	defer ts.Close()
187
-
188
-	var req *http.Request
189
-	req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody)
190
-	assert.NilError(t, err)
191
-
192
-	client := &http.Client{}
193
-	retries := uint32(5)
194
-
195
-	resreq := NewRequestReader(client, req, retries, 0)
196
-	defer resreq.Close()
197
-
198
-	data, err := io.ReadAll(resreq)
199
-	assert.NilError(t, err)
200
-
201
-	resstr := strings.TrimSuffix(string(data), "\n")
202
-	assert.Check(t, is.Equal(srvtxt, resstr))
203
-}
204
-
205
-func TestResumableRequestReader(t *testing.T) {
206
-	srvtxt := "some response text data"
207
-
208
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
209
-		fmt.Fprintln(w, srvtxt)
210
-	}))
211
-	defer ts.Close()
212
-
213
-	var req *http.Request
214
-	req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody)
215
-	assert.NilError(t, err)
216
-
217
-	client := &http.Client{}
218
-	retries := uint32(5)
219
-	imgSize := int64(len(srvtxt))
220
-
221
-	resreq := NewRequestReader(client, req, retries, imgSize)
222
-	defer resreq.Close()
223
-
224
-	data, err := io.ReadAll(resreq)
225
-	assert.NilError(t, err)
226
-
227
-	resstr := strings.TrimSuffix(string(data), "\n")
228
-	assert.Check(t, is.Equal(srvtxt, resstr))
229
-}
230
-
231
-func TestResumableRequestReaderWithInitialResponse(t *testing.T) {
232
-	srvtxt := "some response text data"
233
-
234
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
235
-		fmt.Fprintln(w, srvtxt)
236
-	}))
237
-	defer ts.Close()
238
-
239
-	var req *http.Request
240
-	req, err := http.NewRequest(http.MethodGet, ts.URL, http.NoBody)
241
-	assert.NilError(t, err)
242
-
243
-	client := &http.Client{}
244
-	retries := uint32(5)
245
-	imgSize := int64(len(srvtxt))
246
-
247
-	res, err := client.Do(req)
248
-	assert.NilError(t, err)
249
-
250
-	resreq := NewRequestReaderWithInitialResponse(client, req, retries, imgSize, res)
251
-	defer resreq.Close()
252
-
253
-	data, err := io.ReadAll(resreq)
254
-	assert.NilError(t, err)
255
-
256
-	resstr := strings.TrimSuffix(string(data), "\n")
257
-	assert.Check(t, is.Equal(srvtxt, resstr))
258
-}
259 1
deleted file mode 100644
... ...
@@ -1,170 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"context"
5
-	"net/http"
6
-	"strconv"
7
-	"strings"
8
-
9
-	"github.com/containerd/log"
10
-	"github.com/docker/distribution/registry/client/auth"
11
-	"github.com/moby/moby/api/types/filters"
12
-	"github.com/moby/moby/api/types/registry"
13
-	"github.com/pkg/errors"
14
-)
15
-
16
-var acceptedSearchFilterTags = map[string]bool{
17
-	"is-automated": true, // Deprecated: the "is_automated" field is deprecated and will always be false in the future.
18
-	"is-official":  true,
19
-	"stars":        true,
20
-}
21
-
22
-// Search queries the public registry for repositories matching the specified
23
-// search term and filters.
24
-func (s *Service) Search(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *registry.AuthConfig, headers map[string][]string) ([]registry.SearchResult, error) {
25
-	if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil {
26
-		return nil, err
27
-	}
28
-
29
-	isAutomated, err := searchFilters.GetBoolOrDefault("is-automated", false)
30
-	if err != nil {
31
-		return nil, err
32
-	}
33
-
34
-	// "is-automated" is deprecated and filtering for `true` will yield no results.
35
-	if isAutomated {
36
-		return []registry.SearchResult{}, nil
37
-	}
38
-
39
-	isOfficial, err := searchFilters.GetBoolOrDefault("is-official", false)
40
-	if err != nil {
41
-		return nil, err
42
-	}
43
-
44
-	hasStarFilter := 0
45
-	if searchFilters.Contains("stars") {
46
-		hasStars := searchFilters.Get("stars")
47
-		for _, hasStar := range hasStars {
48
-			iHasStar, err := strconv.Atoi(hasStar)
49
-			if err != nil {
50
-				return nil, invalidParameterErr{errors.Wrapf(err, "invalid filter 'stars=%s'", hasStar)}
51
-			}
52
-			if iHasStar > hasStarFilter {
53
-				hasStarFilter = iHasStar
54
-			}
55
-		}
56
-	}
57
-
58
-	unfilteredResult, err := s.searchUnfiltered(ctx, term, limit, authConfig, headers)
59
-	if err != nil {
60
-		return nil, err
61
-	}
62
-
63
-	filteredResults := []registry.SearchResult{}
64
-	for _, result := range unfilteredResult.Results {
65
-		if searchFilters.Contains("is-official") {
66
-			if isOfficial != result.IsOfficial {
67
-				continue
68
-			}
69
-		}
70
-		if searchFilters.Contains("stars") {
71
-			if result.StarCount < hasStarFilter {
72
-				continue
73
-			}
74
-		}
75
-		// "is-automated" is deprecated and the value in Docker Hub search
76
-		// results is untrustworthy. Force it to false so as to not mislead our
77
-		// clients.
78
-		result.IsAutomated = false //nolint:staticcheck  // ignore SA1019 (field is deprecated)
79
-		filteredResults = append(filteredResults, result)
80
-	}
81
-
82
-	return filteredResults, nil
83
-}
84
-
85
-func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int, authConfig *registry.AuthConfig, headers http.Header) (*registry.SearchResults, error) {
86
-	if hasScheme(term) {
87
-		return nil, invalidParamf("invalid repository name: repository name (%s) should not have a scheme", term)
88
-	}
89
-
90
-	indexName, remoteName := splitReposSearchTerm(term)
91
-
92
-	// Search is a long-running operation, just lock s.config to avoid block others.
93
-	s.mu.RLock()
94
-	index := newIndexInfo(s.config, indexName)
95
-	s.mu.RUnlock()
96
-	if index.Official {
97
-		// If pull "library/foo", it's stored locally under "foo"
98
-		remoteName = strings.TrimPrefix(remoteName, "library/")
99
-	}
100
-
101
-	endpoint, err := newV1Endpoint(ctx, index, headers)
102
-	if err != nil {
103
-		return nil, err
104
-	}
105
-
106
-	var client *http.Client
107
-	if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" {
108
-		creds := NewStaticCredentialStore(authConfig)
109
-
110
-		// TODO(thaJeztah); is there a reason not to include other headers here? (originally added in 19d48f0b8ba59eea9f2cac4ad1c7977712a6b7ac)
111
-		modifiers := Headers(headers.Get("User-Agent"), nil)
112
-		v2Client, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, []auth.Scope{
113
-			auth.RegistryScope{Name: "catalog", Actions: []string{"search"}},
114
-		})
115
-		if err != nil {
116
-			return nil, err
117
-		}
118
-		// Copy non transport http client features
119
-		v2Client.Timeout = endpoint.client.Timeout
120
-		v2Client.CheckRedirect = endpoint.client.CheckRedirect
121
-		v2Client.Jar = endpoint.client.Jar
122
-
123
-		log.G(ctx).Debugf("using v2 client for search to %s", endpoint.URL)
124
-		client = v2Client
125
-	} else {
126
-		client = endpoint.client
127
-		if err := authorizeClient(ctx, client, authConfig, endpoint); err != nil {
128
-			return nil, err
129
-		}
130
-	}
131
-
132
-	return newSession(client, endpoint).searchRepositories(ctx, remoteName, limit)
133
-}
134
-
135
-// splitReposSearchTerm breaks a search term into an index name and remote name
136
-func splitReposSearchTerm(reposName string) (string, string) {
137
-	nameParts := strings.SplitN(reposName, "/", 2)
138
-	if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") &&
139
-		!strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") {
140
-		// This is a Docker Hub repository (ex: samalba/hipache or ubuntu),
141
-		// use the default Docker Hub registry (docker.io)
142
-		return IndexName, reposName
143
-	}
144
-	return nameParts[0], nameParts[1]
145
-}
146
-
147
-// ParseSearchIndexInfo will use repository name to get back an indexInfo.
148
-//
149
-// TODO(thaJeztah) this function is only used by the CLI, and used to get
150
-// information of the registry (to provide credentials if needed). We should
151
-// move this function (or equivalent) to the CLI, as it's doing too much just
152
-// for that.
153
-func ParseSearchIndexInfo(reposName string) (*registry.IndexInfo, error) {
154
-	indexName, _ := splitReposSearchTerm(reposName)
155
-	indexName = normalizeIndexName(indexName)
156
-	if indexName == IndexName {
157
-		return &registry.IndexInfo{
158
-			Name:     IndexName,
159
-			Mirrors:  []string{},
160
-			Secure:   true,
161
-			Official: true,
162
-		}, nil
163
-	}
164
-
165
-	return &registry.IndexInfo{
166
-		Name:    indexName,
167
-		Mirrors: []string{},
168
-		Secure:  !isInsecure(indexName),
169
-	}, nil
170
-}
171 1
deleted file mode 100644
... ...
@@ -1,213 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"context"
5
-	"crypto/tls"
6
-	"encoding/json"
7
-	"errors"
8
-	"fmt"
9
-	"net/http"
10
-	"net/url"
11
-	"strings"
12
-
13
-	"github.com/containerd/log"
14
-	"github.com/docker/distribution/registry/client/transport"
15
-	"github.com/moby/moby/api/types/registry"
16
-)
17
-
18
-// v1PingResult contains the information returned when pinging a registry. It
19
-// indicates whether the registry claims to be a standalone registry.
20
-type v1PingResult struct {
21
-	// Standalone is set to true if the registry indicates it is a
22
-	// standalone registry in the X-Docker-Registry-Standalone
23
-	// header
24
-	Standalone bool `json:"standalone"`
25
-}
26
-
27
-// v1Endpoint stores basic information about a V1 registry endpoint.
28
-type v1Endpoint struct {
29
-	client   *http.Client
30
-	URL      *url.URL
31
-	IsSecure bool
32
-}
33
-
34
-// newV1Endpoint parses the given address to return a registry endpoint.
35
-// TODO: remove. This is only used by search.
36
-func newV1Endpoint(ctx context.Context, index *registry.IndexInfo, headers http.Header) (*v1Endpoint, error) {
37
-	tlsConfig, err := newTLSConfig(ctx, index.Name, index.Secure)
38
-	if err != nil {
39
-		return nil, err
40
-	}
41
-
42
-	endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, headers)
43
-	if err != nil {
44
-		return nil, err
45
-	}
46
-
47
-	if endpoint.String() == IndexServer {
48
-		// Skip the check, we know this one is valid
49
-		// (and we never want to fall back to http in case of error)
50
-		return endpoint, nil
51
-	}
52
-
53
-	// Try HTTPS ping to registry
54
-	endpoint.URL.Scheme = "https"
55
-	if _, err := endpoint.ping(ctx); err != nil {
56
-		if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
57
-			return nil, err
58
-		}
59
-		if endpoint.IsSecure {
60
-			// If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry`
61
-			// in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fall back to HTTP.
62
-			hint := fmt.Sprintf(
63
-				". If this private registry supports only HTTP or HTTPS with an unknown CA certificate, add `--insecure-registry %[1]s` to the daemon's arguments. "+
64
-					"In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; place the CA certificate at /etc/docker/certs.d/%[1]s/ca.crt",
65
-				endpoint.URL.Host,
66
-			)
67
-			return nil, invalidParamf("invalid registry endpoint %s: %v%s", endpoint, err, hint)
68
-		}
69
-
70
-		// registry is insecure and HTTPS failed, fallback to HTTP.
71
-		log.G(ctx).WithError(err).Debugf("error from registry %q marked as insecure - insecurely falling back to HTTP", endpoint)
72
-		endpoint.URL.Scheme = "http"
73
-		if _, err2 := endpoint.ping(ctx); err2 != nil {
74
-			return nil, invalidParamf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
75
-		}
76
-	}
77
-
78
-	return endpoint, nil
79
-}
80
-
81
-// trimV1Address trims the "v1" version suffix off the address and returns
82
-// the trimmed address. It returns an error on "v2" endpoints.
83
-func trimV1Address(address string) (string, error) {
84
-	trimmed := strings.TrimSuffix(address, "/")
85
-	if strings.HasSuffix(trimmed, "/v2") {
86
-		return "", invalidParamf("search is not supported on v2 endpoints: %s", address)
87
-	}
88
-	return strings.TrimSuffix(trimmed, "/v1"), nil
89
-}
90
-
91
-func newV1EndpointFromStr(address string, tlsConfig *tls.Config, headers http.Header) (*v1Endpoint, error) {
92
-	if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
93
-		address = "https://" + address
94
-	}
95
-
96
-	address, err := trimV1Address(address)
97
-	if err != nil {
98
-		return nil, err
99
-	}
100
-
101
-	uri, err := url.Parse(address)
102
-	if err != nil {
103
-		return nil, invalidParam(err)
104
-	}
105
-
106
-	// TODO(tiborvass): make sure a ConnectTimeout transport is used
107
-	tr := newTransport(tlsConfig)
108
-
109
-	return &v1Endpoint{
110
-		IsSecure: tlsConfig == nil || !tlsConfig.InsecureSkipVerify,
111
-		URL:      uri,
112
-		client:   httpClient(transport.NewTransport(tr, Headers("", headers)...)),
113
-	}, nil
114
-}
115
-
116
-// Get the formatted URL for the root of this registry Endpoint
117
-func (e *v1Endpoint) String() string {
118
-	return e.URL.String() + "/v1/"
119
-}
120
-
121
-// ping returns a v1PingResult which indicates whether the registry is standalone or not.
122
-func (e *v1Endpoint) ping(ctx context.Context) (v1PingResult, error) {
123
-	if e.String() == IndexServer {
124
-		// Skip the check, we know this one is valid
125
-		// (and we never want to fallback to http in case of error)
126
-		return v1PingResult{}, nil
127
-	}
128
-
129
-	pingURL := e.String() + "_ping"
130
-	log.G(ctx).WithField("url", pingURL).Debug("attempting v1 ping for registry endpoint")
131
-	req, err := http.NewRequestWithContext(ctx, http.MethodGet, pingURL, http.NoBody)
132
-	if err != nil {
133
-		return v1PingResult{}, invalidParam(err)
134
-	}
135
-
136
-	resp, err := e.client.Do(req)
137
-	if err != nil {
138
-		if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
139
-			return v1PingResult{}, err
140
-		}
141
-		return v1PingResult{}, invalidParam(err)
142
-	}
143
-
144
-	defer resp.Body.Close()
145
-
146
-	if v := resp.Header.Get("X-Docker-Registry-Standalone"); v != "" {
147
-		info := v1PingResult{}
148
-		// Accepted values are "1", and "true" (case-insensitive).
149
-		if v == "1" || strings.EqualFold(v, "true") {
150
-			info.Standalone = true
151
-		}
152
-		log.G(ctx).Debugf("v1PingResult.Standalone (from X-Docker-Registry-Standalone header): %t", info.Standalone)
153
-		return info, nil
154
-	}
155
-
156
-	// If the header is absent, we assume true for compatibility with earlier
157
-	// versions of the registry. default to true
158
-	info := v1PingResult{
159
-		Standalone: true,
160
-	}
161
-	if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
162
-		log.G(ctx).WithError(err).Debug("error unmarshaling _ping response")
163
-		// don't stop here. Just assume sane defaults
164
-	}
165
-
166
-	log.G(ctx).Debugf("v1PingResult.Standalone: %t", info.Standalone)
167
-	return info, nil
168
-}
169
-
170
-// httpClient returns an HTTP client structure which uses the given transport
171
-// and contains the necessary headers for redirected requests
172
-func httpClient(tr http.RoundTripper) *http.Client {
173
-	return &http.Client{
174
-		Transport:     tr,
175
-		CheckRedirect: addRequiredHeadersToRedirectedRequests,
176
-	}
177
-}
178
-
179
-func trustedLocation(req *http.Request) bool {
180
-	var (
181
-		trusteds = []string{"docker.com", "docker.io"}
182
-		hostname = strings.SplitN(req.Host, ":", 2)[0]
183
-	)
184
-	if req.URL.Scheme != "https" {
185
-		return false
186
-	}
187
-
188
-	for _, trusted := range trusteds {
189
-		if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) {
190
-			return true
191
-		}
192
-	}
193
-	return false
194
-}
195
-
196
-// addRequiredHeadersToRedirectedRequests adds the necessary redirection headers
197
-// for redirected requests
198
-func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error {
199
-	if len(via) != 0 && via[0] != nil {
200
-		if trustedLocation(req) && trustedLocation(via[0]) {
201
-			req.Header = via[0].Header
202
-			return nil
203
-		}
204
-		for k, v := range via[0].Header {
205
-			if k != "Authorization" {
206
-				for _, vv := range v {
207
-					req.Header.Add(k, vv)
208
-				}
209
-			}
210
-		}
211
-	}
212
-	return nil
213
-}
214 1
deleted file mode 100644
... ...
@@ -1,237 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"context"
5
-	"net/http"
6
-	"net/http/httptest"
7
-	"strings"
8
-	"testing"
9
-
10
-	"github.com/moby/moby/api/types/registry"
11
-	"gotest.tools/v3/assert"
12
-	is "gotest.tools/v3/assert/cmp"
13
-)
14
-
15
-func TestV1EndpointPing(t *testing.T) {
16
-	testPing := func(index *registry.IndexInfo, expectedStandalone bool, assertMessage string) {
17
-		ep, err := newV1Endpoint(context.Background(), index, nil)
18
-		if err != nil {
19
-			t.Fatal(err)
20
-		}
21
-		regInfo, err := ep.ping(context.Background())
22
-		if err != nil {
23
-			t.Fatal(err)
24
-		}
25
-
26
-		assert.Equal(t, regInfo.Standalone, expectedStandalone, assertMessage)
27
-	}
28
-
29
-	testPing(makeIndex("/v1/"), true, "Expected standalone to be true (default)")
30
-	testPing(makeHTTPSIndex("/v1/"), true, "Expected standalone to be true (default)")
31
-	testPing(makePublicIndex(), false, "Expected standalone to be false for public index")
32
-}
33
-
34
-func TestV1Endpoint(t *testing.T) {
35
-	// Simple wrapper to fail test if err != nil
36
-	expandEndpoint := func(index *registry.IndexInfo) *v1Endpoint {
37
-		endpoint, err := newV1Endpoint(context.Background(), index, nil)
38
-		if err != nil {
39
-			t.Fatal(err)
40
-		}
41
-		return endpoint
42
-	}
43
-
44
-	assertInsecureIndex := func(index *registry.IndexInfo) {
45
-		index.Secure = true
46
-		_, err := newV1Endpoint(context.Background(), index, nil)
47
-		assert.ErrorContains(t, err, "insecure-registry", index.Name+": Expected insecure-registry  error for insecure index")
48
-		index.Secure = false
49
-	}
50
-
51
-	assertSecureIndex := func(index *registry.IndexInfo) {
52
-		index.Secure = true
53
-		_, err := newV1Endpoint(context.Background(), index, nil)
54
-		assert.ErrorContains(t, err, "certificate signed by unknown authority", index.Name+": Expected cert error for secure index")
55
-		index.Secure = false
56
-	}
57
-
58
-	index := &registry.IndexInfo{}
59
-	index.Name = makeURL("/v1/")
60
-	endpoint := expandEndpoint(index)
61
-	assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
62
-	assertInsecureIndex(index)
63
-
64
-	index.Name = makeURL("")
65
-	endpoint = expandEndpoint(index)
66
-	assert.Equal(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
67
-	assertInsecureIndex(index)
68
-
69
-	httpURL := makeURL("")
70
-	index.Name = strings.SplitN(httpURL, "://", 2)[1]
71
-	endpoint = expandEndpoint(index)
72
-	assert.Equal(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/")
73
-	assertInsecureIndex(index)
74
-
75
-	index.Name = makeHTTPSURL("/v1/")
76
-	endpoint = expandEndpoint(index)
77
-	assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
78
-	assertSecureIndex(index)
79
-
80
-	index.Name = makeHTTPSURL("")
81
-	endpoint = expandEndpoint(index)
82
-	assert.Equal(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
83
-	assertSecureIndex(index)
84
-
85
-	httpsURL := makeHTTPSURL("")
86
-	index.Name = strings.SplitN(httpsURL, "://", 2)[1]
87
-	endpoint = expandEndpoint(index)
88
-	assert.Equal(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/")
89
-	assertSecureIndex(index)
90
-
91
-	badEndpoints := []string{
92
-		"http://127.0.0.1/v1/",
93
-		"https://127.0.0.1/v1/",
94
-		"http://127.0.0.1",
95
-		"https://127.0.0.1",
96
-		"127.0.0.1",
97
-	}
98
-	for _, address := range badEndpoints {
99
-		index.Name = address
100
-		_, err := newV1Endpoint(context.Background(), index, nil)
101
-		assert.Check(t, err != nil, "Expected error while expanding bad endpoint: %s", address)
102
-	}
103
-}
104
-
105
-func TestV1EndpointParse(t *testing.T) {
106
-	tests := []struct {
107
-		address     string
108
-		expected    string
109
-		expectedErr string
110
-	}{
111
-		{
112
-			address:  IndexServer,
113
-			expected: IndexServer,
114
-		},
115
-		{
116
-			address:  "https://0.0.0.0:5000/v1/",
117
-			expected: "https://0.0.0.0:5000/v1/",
118
-		},
119
-		{
120
-			address:  "https://0.0.0.0:5000",
121
-			expected: "https://0.0.0.0:5000/v1/",
122
-		},
123
-		{
124
-			address:  "0.0.0.0:5000",
125
-			expected: "https://0.0.0.0:5000/v1/",
126
-		},
127
-		{
128
-			address:  "https://0.0.0.0:5000/nonversion/",
129
-			expected: "https://0.0.0.0:5000/nonversion/v1/",
130
-		},
131
-		{
132
-			address:  "https://0.0.0.0:5000/v0/",
133
-			expected: "https://0.0.0.0:5000/v0/v1/",
134
-		},
135
-		{
136
-			address:     "https://0.0.0.0:5000/v2/",
137
-			expectedErr: "search is not supported on v2 endpoints: https://0.0.0.0:5000/v2/",
138
-		},
139
-	}
140
-	for _, tc := range tests {
141
-		t.Run(tc.address, func(t *testing.T) {
142
-			ep, err := newV1EndpointFromStr(tc.address, nil, nil)
143
-			if tc.expectedErr != "" {
144
-				assert.Check(t, is.Error(err, tc.expectedErr))
145
-				assert.Check(t, is.Nil(ep))
146
-			} else {
147
-				assert.NilError(t, err)
148
-				assert.Check(t, is.Equal(ep.String(), tc.expected))
149
-			}
150
-		})
151
-	}
152
-}
153
-
154
-// Ensure that a registry endpoint that responds with a 401 only is determined
155
-// to be a valid v1 registry endpoint
156
-func TestV1EndpointValidate(t *testing.T) {
157
-	requireBasicAuthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
158
-		w.Header().Add("WWW-Authenticate", `Basic realm="localhost"`)
159
-		w.WriteHeader(http.StatusUnauthorized)
160
-	})
161
-
162
-	// Make a test server which should validate as a v1 server.
163
-	testServer := httptest.NewServer(requireBasicAuthHandler)
164
-	defer testServer.Close()
165
-
166
-	testEndpoint, err := newV1Endpoint(context.Background(), &registry.IndexInfo{Name: testServer.URL}, nil)
167
-	if err != nil {
168
-		t.Fatal(err)
169
-	}
170
-
171
-	if testEndpoint.URL.Scheme != "http" {
172
-		t.Fatalf("expecting to validate endpoint as http, got url %s", testEndpoint.String())
173
-	}
174
-}
175
-
176
-func TestTrustedLocation(t *testing.T) {
177
-	for _, u := range []string{"http://example.com", "https://example.com:7777", "http://docker.io", "http://test.docker.com", "https://fakedocker.com"} {
178
-		req, _ := http.NewRequest(http.MethodGet, u, http.NoBody)
179
-		assert.Check(t, !trustedLocation(req))
180
-	}
181
-
182
-	for _, u := range []string{"https://docker.io", "https://test.docker.com:80"} {
183
-		req, _ := http.NewRequest(http.MethodGet, u, http.NoBody)
184
-		assert.Check(t, trustedLocation(req))
185
-	}
186
-}
187
-
188
-func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) {
189
-	for _, urls := range [][]string{
190
-		{"http://docker.io", "https://docker.com"},
191
-		{"https://foo.docker.io:7777", "http://bar.docker.com"},
192
-		{"https://foo.docker.io", "https://example.com"},
193
-	} {
194
-		reqFrom, _ := http.NewRequest(http.MethodGet, urls[0], http.NoBody)
195
-		reqFrom.Header.Add("Content-Type", "application/json")
196
-		reqFrom.Header.Add("Authorization", "super_secret")
197
-		reqTo, _ := http.NewRequest(http.MethodGet, urls[1], http.NoBody)
198
-
199
-		_ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
200
-
201
-		if len(reqTo.Header) != 1 {
202
-			t.Fatalf("Expected 1 headers, got %d", len(reqTo.Header))
203
-		}
204
-
205
-		if reqTo.Header.Get("Content-Type") != "application/json" {
206
-			t.Fatal("'Content-Type' should be 'application/json'")
207
-		}
208
-
209
-		if reqTo.Header.Get("Authorization") != "" {
210
-			t.Fatal("'Authorization' should be empty")
211
-		}
212
-	}
213
-
214
-	for _, urls := range [][]string{
215
-		{"https://docker.io", "https://docker.com"},
216
-		{"https://foo.docker.io:7777", "https://bar.docker.com"},
217
-	} {
218
-		reqFrom, _ := http.NewRequest(http.MethodGet, urls[0], http.NoBody)
219
-		reqFrom.Header.Add("Content-Type", "application/json")
220
-		reqFrom.Header.Add("Authorization", "super_secret")
221
-		reqTo, _ := http.NewRequest(http.MethodGet, urls[1], http.NoBody)
222
-
223
-		_ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
224
-
225
-		if len(reqTo.Header) != 2 {
226
-			t.Fatalf("Expected 2 headers, got %d", len(reqTo.Header))
227
-		}
228
-
229
-		if reqTo.Header.Get("Content-Type") != "application/json" {
230
-			t.Fatal("'Content-Type' should be 'application/json'")
231
-		}
232
-
233
-		if reqTo.Header.Get("Authorization") != "super_secret" {
234
-			t.Fatal("'Authorization' should be 'super_secret'")
235
-		}
236
-	}
237
-}
238 1
deleted file mode 100644
... ...
@@ -1,248 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	// this is required for some certificates
5
-	"context"
6
-	_ "crypto/sha512"
7
-	"encoding/json"
8
-	"fmt"
9
-	"io"
10
-	"net/http"
11
-	"net/http/cookiejar"
12
-	"net/url"
13
-	"strconv"
14
-	"strings"
15
-	"sync"
16
-
17
-	"github.com/containerd/log"
18
-	"github.com/moby/moby/api/types/registry"
19
-	"github.com/pkg/errors"
20
-)
21
-
22
-// A session is used to communicate with a V1 registry
23
-type session struct {
24
-	indexEndpoint *v1Endpoint
25
-	client        *http.Client
26
-}
27
-
28
-type authTransport struct {
29
-	base       http.RoundTripper
30
-	authConfig *registry.AuthConfig
31
-
32
-	alwaysSetBasicAuth bool
33
-	token              []string
34
-
35
-	mu     sync.Mutex                      // guards modReq
36
-	modReq map[*http.Request]*http.Request // original -> modified
37
-}
38
-
39
-// newAuthTransport handles the auth layer when communicating with a v1 registry (private or official)
40
-//
41
-// For private v1 registries, set alwaysSetBasicAuth to true.
42
-//
43
-// For the official v1 registry, if there isn't already an Authorization header in the request,
44
-// but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header.
45
-// After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing
46
-// a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent
47
-// requests.
48
-//
49
-// If the server sends a token without the client having requested it, it is ignored.
50
-//
51
-// This RoundTripper also has a CancelRequest method important for correct timeout handling.
52
-func newAuthTransport(base http.RoundTripper, authConfig *registry.AuthConfig, alwaysSetBasicAuth bool) *authTransport {
53
-	if base == nil {
54
-		base = http.DefaultTransport
55
-	}
56
-	return &authTransport{
57
-		base:               base,
58
-		authConfig:         authConfig,
59
-		alwaysSetBasicAuth: alwaysSetBasicAuth,
60
-		modReq:             make(map[*http.Request]*http.Request),
61
-	}
62
-}
63
-
64
-// cloneRequest returns a clone of the provided *http.Request.
65
-// The clone is a shallow copy of the struct and its Header map.
66
-func cloneRequest(r *http.Request) *http.Request {
67
-	// shallow copy of the struct
68
-	r2 := new(http.Request)
69
-	*r2 = *r
70
-	// deep copy of the Header
71
-	r2.Header = make(http.Header, len(r.Header))
72
-	for k, s := range r.Header {
73
-		r2.Header[k] = append([]string(nil), s...)
74
-	}
75
-
76
-	return r2
77
-}
78
-
79
-// onEOFReader wraps an io.ReadCloser and a function
80
-// the function will run at the end of file or close the file.
81
-type onEOFReader struct {
82
-	Rc io.ReadCloser
83
-	Fn func()
84
-}
85
-
86
-func (r *onEOFReader) Read(p []byte) (int, error) {
87
-	n, err := r.Rc.Read(p)
88
-	if err == io.EOF {
89
-		r.runFunc()
90
-	}
91
-	return n, err
92
-}
93
-
94
-// Close closes the file and run the function.
95
-func (r *onEOFReader) Close() error {
96
-	err := r.Rc.Close()
97
-	r.runFunc()
98
-	return err
99
-}
100
-
101
-func (r *onEOFReader) runFunc() {
102
-	if fn := r.Fn; fn != nil {
103
-		fn()
104
-		r.Fn = nil
105
-	}
106
-}
107
-
108
-// RoundTrip changes an HTTP request's headers to add the necessary
109
-// authentication-related headers
110
-func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) {
111
-	// Authorization should not be set on 302 redirect for untrusted locations.
112
-	// This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests.
113
-	// As the authorization logic is currently implemented in RoundTrip,
114
-	// a 302 redirect is detected by looking at the Referrer header as go http package adds said header.
115
-	// This is safe as Docker doesn't set Referrer in other scenarios.
116
-	if orig.Header.Get("Referer") != "" && !trustedLocation(orig) {
117
-		return tr.base.RoundTrip(orig)
118
-	}
119
-
120
-	req := cloneRequest(orig)
121
-	tr.mu.Lock()
122
-	tr.modReq[orig] = req
123
-	tr.mu.Unlock()
124
-
125
-	if tr.alwaysSetBasicAuth {
126
-		if tr.authConfig == nil {
127
-			return nil, errors.New("unexpected error: empty auth config")
128
-		}
129
-		req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password)
130
-		return tr.base.RoundTrip(req)
131
-	}
132
-
133
-	// Don't override
134
-	if req.Header.Get("Authorization") == "" {
135
-		if req.Header.Get("X-Docker-Token") == "true" && tr.authConfig != nil && tr.authConfig.Username != "" {
136
-			req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password)
137
-		} else if len(tr.token) > 0 {
138
-			req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ","))
139
-		}
140
-	}
141
-	resp, err := tr.base.RoundTrip(req)
142
-	if err != nil {
143
-		tr.mu.Lock()
144
-		delete(tr.modReq, orig)
145
-		tr.mu.Unlock()
146
-		return nil, err
147
-	}
148
-	if len(resp.Header["X-Docker-Token"]) > 0 {
149
-		tr.token = resp.Header["X-Docker-Token"]
150
-	}
151
-	resp.Body = &onEOFReader{
152
-		Rc: resp.Body,
153
-		Fn: func() {
154
-			tr.mu.Lock()
155
-			delete(tr.modReq, orig)
156
-			tr.mu.Unlock()
157
-		},
158
-	}
159
-	return resp, nil
160
-}
161
-
162
-// CancelRequest cancels an in-flight request by closing its connection.
163
-func (tr *authTransport) CancelRequest(req *http.Request) {
164
-	type canceler interface {
165
-		CancelRequest(*http.Request)
166
-	}
167
-	if cr, ok := tr.base.(canceler); ok {
168
-		tr.mu.Lock()
169
-		modReq := tr.modReq[req]
170
-		delete(tr.modReq, req)
171
-		tr.mu.Unlock()
172
-		cr.CancelRequest(modReq)
173
-	}
174
-}
175
-
176
-func authorizeClient(ctx context.Context, client *http.Client, authConfig *registry.AuthConfig, endpoint *v1Endpoint) error {
177
-	var alwaysSetBasicAuth bool
178
-
179
-	// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
180
-	// alongside all our requests.
181
-	if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" {
182
-		info, err := endpoint.ping(ctx)
183
-		if err != nil {
184
-			return err
185
-		}
186
-		if info.Standalone && authConfig != nil {
187
-			log.G(ctx).WithField("endpoint", endpoint.String()).Debug("Endpoint is eligible for private registry; enabling alwaysSetBasicAuth")
188
-			alwaysSetBasicAuth = true
189
-		}
190
-	}
191
-
192
-	// Annotate the transport unconditionally so that v2 can
193
-	// properly fallback on v1 when an image is not found.
194
-	client.Transport = newAuthTransport(client.Transport, authConfig, alwaysSetBasicAuth)
195
-
196
-	jar, err := cookiejar.New(nil)
197
-	if err != nil {
198
-		return systemErr{errors.New("cookiejar.New is not supposed to return an error")}
199
-	}
200
-	client.Jar = jar
201
-
202
-	return nil
203
-}
204
-
205
-func newSession(client *http.Client, endpoint *v1Endpoint) *session {
206
-	return &session{
207
-		client:        client,
208
-		indexEndpoint: endpoint,
209
-	}
210
-}
211
-
212
-// defaultSearchLimit is the default value for maximum number of returned search results.
213
-const defaultSearchLimit = 25
214
-
215
-// searchRepositories performs a search against the remote repository
216
-func (r *session) searchRepositories(ctx context.Context, term string, limit int) (*registry.SearchResults, error) {
217
-	if limit == 0 {
218
-		limit = defaultSearchLimit
219
-	}
220
-	if limit < 1 || limit > 100 {
221
-		return nil, invalidParamf("limit %d is outside the range of [1, 100]", limit)
222
-	}
223
-	u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(strconv.Itoa(limit))
224
-	log.G(ctx).WithField("url", u).Debug("searchRepositories")
225
-
226
-	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, http.NoBody)
227
-	if err != nil {
228
-		return nil, invalidParamWrapf(err, "error building request")
229
-	}
230
-	// Have the AuthTransport send authentication, when logged in.
231
-	req.Header.Set("X-Docker-Token", "true")
232
-	res, err := r.client.Do(req)
233
-	if err != nil {
234
-		return nil, systemErr{err}
235
-	}
236
-	defer res.Body.Close()
237
-	if res.StatusCode != http.StatusOK {
238
-		// TODO(thaJeztah): return upstream response body for errors (see https://github.com/moby/moby/issues/27286).
239
-		// TODO(thaJeztah): handle other status-codes to return correct error-type
240
-		return nil, errUnknown{fmt.Errorf("unexpected status code %d", res.StatusCode)}
241
-	}
242
-	result := &registry.SearchResults{}
243
-	err = json.NewDecoder(res.Body).Decode(result)
244
-	if err != nil {
245
-		return nil, systemErr{errors.Wrap(err, "error decoding registry search results")}
246
-	}
247
-	return result, nil
248
-}
249 1
deleted file mode 100644
... ...
@@ -1,418 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"context"
5
-	"encoding/json"
6
-	"net/http"
7
-	"net/http/httptest"
8
-	"net/http/httputil"
9
-	"testing"
10
-
11
-	cerrdefs "github.com/containerd/errdefs"
12
-	"github.com/docker/distribution/registry/client/transport"
13
-	"github.com/moby/moby/api/types/filters"
14
-	"github.com/moby/moby/api/types/registry"
15
-	"gotest.tools/v3/assert"
16
-)
17
-
18
-func spawnTestRegistrySession(t *testing.T) *session {
19
-	t.Helper()
20
-	authConfig := &registry.AuthConfig{}
21
-	endpoint, err := newV1Endpoint(context.Background(), makeIndex("/v1/"), nil)
22
-	if err != nil {
23
-		t.Fatal(err)
24
-	}
25
-	userAgent := "docker test client"
26
-	var tr http.RoundTripper = debugTransport{newTransport(nil), t.Log}
27
-	tr = transport.NewTransport(newAuthTransport(tr, authConfig, false), Headers(userAgent, nil)...)
28
-	client := httpClient(tr)
29
-
30
-	if err := authorizeClient(context.Background(), client, authConfig, endpoint); err != nil {
31
-		t.Fatal(err)
32
-	}
33
-	r := newSession(client, endpoint)
34
-
35
-	// In a normal scenario for the v1 registry, the client should send a `X-Docker-Token: true`
36
-	// header while authenticating, in order to retrieve a token that can be later used to
37
-	// perform authenticated actions.
38
-	//
39
-	// The mock v1 registry does not support that, (TODO(tiborvass): support it), instead,
40
-	// it will consider authenticated any request with the header `X-Docker-Token: fake-token`.
41
-	//
42
-	// Because we know that the client's transport is an `*authTransport` we simply cast it,
43
-	// in order to set the internal cached token to the fake token, and thus send that fake token
44
-	// upon every subsequent requests.
45
-	r.client.Transport.(*authTransport).token = []string{"fake-token"}
46
-	return r
47
-}
48
-
49
-type debugTransport struct {
50
-	http.RoundTripper
51
-	log func(...interface{})
52
-}
53
-
54
-func (tr debugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
55
-	dump, err := httputil.DumpRequestOut(req, false)
56
-	if err != nil {
57
-		tr.log("could not dump request")
58
-	}
59
-	tr.log(string(dump))
60
-	resp, err := tr.RoundTripper.RoundTrip(req)
61
-	if err != nil {
62
-		return nil, err
63
-	}
64
-	dump, err = httputil.DumpResponse(resp, false)
65
-	if err != nil {
66
-		tr.log("could not dump response")
67
-	}
68
-	tr.log(string(dump))
69
-	return resp, err
70
-}
71
-
72
-func TestSearchRepositories(t *testing.T) {
73
-	r := spawnTestRegistrySession(t)
74
-	results, err := r.searchRepositories(context.Background(), "fakequery", 25)
75
-	if err != nil {
76
-		t.Fatal(err)
77
-	}
78
-	if results == nil {
79
-		t.Fatal("Expected non-nil SearchResults object")
80
-	}
81
-	assert.Equal(t, results.NumResults, 1, "Expected 1 search results")
82
-	assert.Equal(t, results.Query, "fakequery", "Expected 'fakequery' as query")
83
-	assert.Equal(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' to have 42 stars")
84
-}
85
-
86
-func TestSearchErrors(t *testing.T) {
87
-	errorCases := []struct {
88
-		filtersArgs       filters.Args
89
-		shouldReturnError bool
90
-		expectedError     string
91
-	}{
92
-		{
93
-			expectedError:     "unexpected status code 500",
94
-			shouldReturnError: true,
95
-		},
96
-		{
97
-			filtersArgs:   filters.NewArgs(filters.Arg("type", "custom")),
98
-			expectedError: "invalid filter 'type'",
99
-		},
100
-		{
101
-			filtersArgs:   filters.NewArgs(filters.Arg("is-automated", "invalid")),
102
-			expectedError: "invalid filter 'is-automated=[invalid]'",
103
-		},
104
-		{
105
-			filtersArgs: filters.NewArgs(
106
-				filters.Arg("is-automated", "true"),
107
-				filters.Arg("is-automated", "false"),
108
-			),
109
-			expectedError: "invalid filter 'is-automated",
110
-		},
111
-		{
112
-			filtersArgs:   filters.NewArgs(filters.Arg("is-official", "invalid")),
113
-			expectedError: "invalid filter 'is-official=[invalid]'",
114
-		},
115
-		{
116
-			filtersArgs: filters.NewArgs(
117
-				filters.Arg("is-official", "true"),
118
-				filters.Arg("is-official", "false"),
119
-			),
120
-			expectedError: "invalid filter 'is-official",
121
-		},
122
-		{
123
-			filtersArgs:   filters.NewArgs(filters.Arg("stars", "invalid")),
124
-			expectedError: "invalid filter 'stars=invalid'",
125
-		},
126
-		{
127
-			filtersArgs: filters.NewArgs(
128
-				filters.Arg("stars", "1"),
129
-				filters.Arg("stars", "invalid"),
130
-			),
131
-			expectedError: "invalid filter 'stars=invalid'",
132
-		},
133
-	}
134
-	for _, tc := range errorCases {
135
-		t.Run(tc.expectedError, func(t *testing.T) {
136
-			srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
137
-				if !tc.shouldReturnError {
138
-					t.Errorf("unexpected HTTP request")
139
-				}
140
-				http.Error(w, "no search for you", http.StatusInternalServerError)
141
-			}))
142
-			defer srv.Close()
143
-
144
-			// Construct the search term by cutting the 'http://' prefix off srv.URL.
145
-			term := srv.URL[7:] + "/term"
146
-
147
-			reg, err := NewService(ServiceOptions{})
148
-			assert.NilError(t, err)
149
-			_, err = reg.Search(context.Background(), tc.filtersArgs, term, 0, nil, map[string][]string{})
150
-			assert.ErrorContains(t, err, tc.expectedError)
151
-			if tc.shouldReturnError {
152
-				assert.Check(t, cerrdefs.IsUnknown(err), "got: %T: %v", err, err)
153
-				return
154
-			}
155
-			assert.Check(t, cerrdefs.IsInvalidArgument(err), "got: %T: %v", err, err)
156
-		})
157
-	}
158
-}
159
-
160
-func TestSearch(t *testing.T) {
161
-	const term = "term"
162
-	successCases := []struct {
163
-		name            string
164
-		filtersArgs     filters.Args
165
-		registryResults []registry.SearchResult
166
-		expectedResults []registry.SearchResult
167
-	}{
168
-		{
169
-			name:            "empty results",
170
-			registryResults: []registry.SearchResult{},
171
-			expectedResults: []registry.SearchResult{},
172
-		},
173
-		{
174
-			name: "no filter",
175
-			registryResults: []registry.SearchResult{
176
-				{
177
-					Name:        "name",
178
-					Description: "description",
179
-				},
180
-			},
181
-			expectedResults: []registry.SearchResult{
182
-				{
183
-					Name:        "name",
184
-					Description: "description",
185
-				},
186
-			},
187
-		},
188
-		{
189
-			name:        "is-automated=true, no results",
190
-			filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")),
191
-			registryResults: []registry.SearchResult{
192
-				{
193
-					Name:        "name",
194
-					Description: "description",
195
-				},
196
-			},
197
-			expectedResults: []registry.SearchResult{},
198
-		},
199
-		{
200
-			name:        "is-automated=true",
201
-			filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")),
202
-			registryResults: []registry.SearchResult{
203
-				{
204
-					Name:        "name",
205
-					Description: "description",
206
-					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
207
-				},
208
-			},
209
-			expectedResults: []registry.SearchResult{},
210
-		},
211
-		{
212
-			name:        "is-automated=false, IsAutomated reset to false",
213
-			filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")),
214
-			registryResults: []registry.SearchResult{
215
-				{
216
-					Name:        "name",
217
-					Description: "description",
218
-					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
219
-				},
220
-			},
221
-			expectedResults: []registry.SearchResult{
222
-				{
223
-					Name:        "name",
224
-					Description: "description",
225
-					IsAutomated: false, //nolint:staticcheck // ignore SA1019 (field is deprecated).
226
-				},
227
-			},
228
-		},
229
-		{
230
-			name:        "is-automated=false",
231
-			filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")),
232
-			registryResults: []registry.SearchResult{
233
-				{
234
-					Name:        "name",
235
-					Description: "description",
236
-				},
237
-			},
238
-			expectedResults: []registry.SearchResult{
239
-				{
240
-					Name:        "name",
241
-					Description: "description",
242
-				},
243
-			},
244
-		},
245
-		{
246
-			name:        "is-official=true, no results",
247
-			filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")),
248
-			registryResults: []registry.SearchResult{
249
-				{
250
-					Name:        "name",
251
-					Description: "description",
252
-				},
253
-			},
254
-			expectedResults: []registry.SearchResult{},
255
-		},
256
-		{
257
-			name:        "is-official=true",
258
-			filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")),
259
-			registryResults: []registry.SearchResult{
260
-				{
261
-					Name:        "name",
262
-					Description: "description",
263
-					IsOfficial:  true,
264
-				},
265
-			},
266
-			expectedResults: []registry.SearchResult{
267
-				{
268
-					Name:        "name",
269
-					Description: "description",
270
-					IsOfficial:  true,
271
-				},
272
-			},
273
-		},
274
-		{
275
-			name:        "is-official=false, no results",
276
-			filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")),
277
-			registryResults: []registry.SearchResult{
278
-				{
279
-					Name:        "name",
280
-					Description: "description",
281
-					IsOfficial:  true,
282
-				},
283
-			},
284
-			expectedResults: []registry.SearchResult{},
285
-		},
286
-		{
287
-			name:        "is-official=false",
288
-			filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")),
289
-			registryResults: []registry.SearchResult{
290
-				{
291
-					Name:        "name",
292
-					Description: "description",
293
-					IsOfficial:  false,
294
-				},
295
-			},
296
-			expectedResults: []registry.SearchResult{
297
-				{
298
-					Name:        "name",
299
-					Description: "description",
300
-					IsOfficial:  false,
301
-				},
302
-			},
303
-		},
304
-		{
305
-			name:        "stars=0",
306
-			filtersArgs: filters.NewArgs(filters.Arg("stars", "0")),
307
-			registryResults: []registry.SearchResult{
308
-				{
309
-					Name:        "name",
310
-					Description: "description",
311
-					StarCount:   0,
312
-				},
313
-			},
314
-			expectedResults: []registry.SearchResult{
315
-				{
316
-					Name:        "name",
317
-					Description: "description",
318
-					StarCount:   0,
319
-				},
320
-			},
321
-		},
322
-		{
323
-			name:        "stars=0, no results",
324
-			filtersArgs: filters.NewArgs(filters.Arg("stars", "1")),
325
-			registryResults: []registry.SearchResult{
326
-				{
327
-					Name:        "name",
328
-					Description: "description",
329
-					StarCount:   0,
330
-				},
331
-			},
332
-			expectedResults: []registry.SearchResult{},
333
-		},
334
-		{
335
-			name:        "stars=1",
336
-			filtersArgs: filters.NewArgs(filters.Arg("stars", "1")),
337
-			registryResults: []registry.SearchResult{
338
-				{
339
-					Name:        "name0",
340
-					Description: "description0",
341
-					StarCount:   0,
342
-				},
343
-				{
344
-					Name:        "name1",
345
-					Description: "description1",
346
-					StarCount:   1,
347
-				},
348
-			},
349
-			expectedResults: []registry.SearchResult{
350
-				{
351
-					Name:        "name1",
352
-					Description: "description1",
353
-					StarCount:   1,
354
-				},
355
-			},
356
-		},
357
-		{
358
-			name: "stars=1, is-official=true, is-automated=true",
359
-			filtersArgs: filters.NewArgs(
360
-				filters.Arg("stars", "1"),
361
-				filters.Arg("is-official", "true"),
362
-				filters.Arg("is-automated", "true"),
363
-			),
364
-			registryResults: []registry.SearchResult{
365
-				{
366
-					Name:        "name0",
367
-					Description: "description0",
368
-					StarCount:   0,
369
-					IsOfficial:  true,
370
-					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
371
-				},
372
-				{
373
-					Name:        "name1",
374
-					Description: "description1",
375
-					StarCount:   1,
376
-					IsOfficial:  false,
377
-					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
378
-				},
379
-				{
380
-					Name:        "name2",
381
-					Description: "description2",
382
-					StarCount:   1,
383
-					IsOfficial:  true,
384
-				},
385
-				{
386
-					Name:        "name3",
387
-					Description: "description3",
388
-					StarCount:   2,
389
-					IsOfficial:  true,
390
-					IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
391
-				},
392
-			},
393
-			expectedResults: []registry.SearchResult{},
394
-		},
395
-	}
396
-	for _, tc := range successCases {
397
-		t.Run(tc.name, func(t *testing.T) {
398
-			srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
399
-				w.Header().Set("Content-type", "application/json")
400
-				json.NewEncoder(w).Encode(registry.SearchResults{
401
-					Query:      term,
402
-					NumResults: len(tc.registryResults),
403
-					Results:    tc.registryResults,
404
-				})
405
-			}))
406
-			defer srv.Close()
407
-
408
-			// Construct the search term by cutting the 'http://' prefix off srv.URL.
409
-			searchTerm := srv.URL[7:] + "/" + term
410
-
411
-			reg, err := NewService(ServiceOptions{})
412
-			assert.NilError(t, err)
413
-			results, err := reg.Search(context.Background(), tc.filtersArgs, searchTerm, 0, nil, map[string][]string{})
414
-			assert.NilError(t, err)
415
-			assert.DeepEqual(t, results, tc.expectedResults)
416
-		})
417
-	}
418
-}
419 1
deleted file mode 100644
... ...
@@ -1,160 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"context"
5
-	"crypto/tls"
6
-	"errors"
7
-	"net/url"
8
-	"strings"
9
-	"sync"
10
-
11
-	cerrdefs "github.com/containerd/errdefs"
12
-	"github.com/containerd/log"
13
-	"github.com/distribution/reference"
14
-	"github.com/moby/moby/api/types/registry"
15
-)
16
-
17
-// Service is a registry service. It tracks configuration data such as a list
18
-// of mirrors.
19
-type Service struct {
20
-	config *serviceConfig
21
-	mu     sync.RWMutex
22
-}
23
-
24
-// NewService returns a new instance of [Service] ready to be installed into
25
-// an engine.
26
-func NewService(options ServiceOptions) (*Service, error) {
27
-	config, err := newServiceConfig(options)
28
-	if err != nil {
29
-		return nil, err
30
-	}
31
-
32
-	return &Service{config: config}, err
33
-}
34
-
35
-// ServiceConfig returns a copy of the public registry service's configuration.
36
-func (s *Service) ServiceConfig() *registry.ServiceConfig {
37
-	s.mu.RLock()
38
-	defer s.mu.RUnlock()
39
-	return s.config.copy()
40
-}
41
-
42
-// ReplaceConfig prepares a transaction which will atomically replace the
43
-// registry service's configuration when the returned commit function is called.
44
-func (s *Service) ReplaceConfig(options ServiceOptions) (commit func(), _ error) {
45
-	config, err := newServiceConfig(options)
46
-	if err != nil {
47
-		return nil, err
48
-	}
49
-	return func() {
50
-		s.mu.Lock()
51
-		defer s.mu.Unlock()
52
-		s.config = config
53
-	}, nil
54
-}
55
-
56
-// Auth contacts the public registry with the provided credentials,
57
-// and returns OK if authentication was successful.
58
-// It can be used to verify the validity of a client's credentials.
59
-func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (statusMessage, token string, _ error) {
60
-	// TODO Use ctx when searching for repositories
61
-	registryHostName := IndexHostname
62
-
63
-	if authConfig.ServerAddress != "" {
64
-		serverAddress := authConfig.ServerAddress
65
-		if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") {
66
-			serverAddress = "https://" + serverAddress
67
-		}
68
-		u, err := url.Parse(serverAddress)
69
-		if err != nil {
70
-			return "", "", invalidParamWrapf(err, "unable to parse server address")
71
-		}
72
-		registryHostName = u.Host
73
-	}
74
-
75
-	// Lookup endpoints for authentication but exclude mirrors to prevent
76
-	// sending credentials of the upstream registry to a mirror.
77
-	s.mu.RLock()
78
-	endpoints, err := s.lookupV2Endpoints(ctx, registryHostName, false)
79
-	s.mu.RUnlock()
80
-	if err != nil {
81
-		if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
82
-			return "", "", err
83
-		}
84
-		return "", "", invalidParam(err)
85
-	}
86
-
87
-	var lastErr error
88
-	for _, endpoint := range endpoints {
89
-		authToken, err := loginV2(ctx, authConfig, endpoint, userAgent)
90
-		if err != nil {
91
-			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || cerrdefs.IsUnauthorized(err) {
92
-				// Failed to authenticate; don't continue with (non-TLS) endpoints.
93
-				return "", "", err
94
-			}
95
-			// Try next endpoint
96
-			log.G(ctx).WithFields(log.Fields{
97
-				"error":    err,
98
-				"endpoint": endpoint,
99
-			}).Infof("Error logging in to endpoint, trying next endpoint")
100
-			lastErr = err
101
-			continue
102
-		}
103
-
104
-		// TODO(thaJeztah): move the statusMessage to the API endpoint; we don't need to produce that here?
105
-		return "Login Succeeded", authToken, nil
106
-	}
107
-
108
-	return "", "", lastErr
109
-}
110
-
111
-// ResolveAuthConfig looks up authentication for the given reference from the
112
-// given authConfigs.
113
-//
114
-// IMPORTANT: This function is for internal use and should not be used by external projects.
115
-func (s *Service) ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, ref reference.Named) registry.AuthConfig {
116
-	s.mu.RLock()
117
-	defer s.mu.RUnlock()
118
-	// Simplified version of "newIndexInfo" without handling of insecure
119
-	// registries and mirrors, as we don't need that information to resolve
120
-	// the auth-config.
121
-	indexName := normalizeIndexName(reference.Domain(ref))
122
-	registryInfo, ok := s.config.IndexConfigs[indexName]
123
-	if !ok {
124
-		registryInfo = &registry.IndexInfo{Name: indexName}
125
-	}
126
-	return ResolveAuthConfig(authConfigs, registryInfo)
127
-}
128
-
129
-// APIEndpoint represents a remote API endpoint
130
-type APIEndpoint struct {
131
-	Mirror    bool
132
-	URL       *url.URL
133
-	TLSConfig *tls.Config
134
-}
135
-
136
-// LookupPullEndpoints creates a list of v2 endpoints to try to pull from, in order of preference.
137
-// It gives preference to mirrors over the actual registry, and HTTPS over plain HTTP.
138
-func (s *Service) LookupPullEndpoints(hostname string) ([]APIEndpoint, error) {
139
-	s.mu.RLock()
140
-	defer s.mu.RUnlock()
141
-
142
-	return s.lookupV2Endpoints(context.TODO(), hostname, true)
143
-}
144
-
145
-// LookupPushEndpoints creates a list of v2 endpoints to try to push to, in order of preference.
146
-// It gives preference to HTTPS over plain HTTP. Mirrors are not included.
147
-func (s *Service) LookupPushEndpoints(hostname string) ([]APIEndpoint, error) {
148
-	s.mu.RLock()
149
-	defer s.mu.RUnlock()
150
-
151
-	return s.lookupV2Endpoints(context.TODO(), hostname, false)
152
-}
153
-
154
-// IsInsecureRegistry returns true if the registry at given host is configured as
155
-// insecure registry.
156
-func (s *Service) IsInsecureRegistry(host string) bool {
157
-	s.mu.RLock()
158
-	defer s.mu.RUnlock()
159
-	return !s.config.isSecureIndex(host)
160
-}
161 1
deleted file mode 100644
... ...
@@ -1,73 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"context"
5
-	"net/url"
6
-	"strings"
7
-
8
-	"github.com/docker/go-connections/tlsconfig"
9
-)
10
-
11
-func (s *Service) lookupV2Endpoints(ctx context.Context, hostname string, includeMirrors bool) ([]APIEndpoint, error) {
12
-	var endpoints []APIEndpoint
13
-	if hostname == DefaultNamespace || hostname == IndexHostname {
14
-		if includeMirrors {
15
-			for _, mirror := range s.config.Mirrors {
16
-				if ctx.Err() != nil {
17
-					return nil, ctx.Err()
18
-				}
19
-				if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {
20
-					mirror = "https://" + mirror
21
-				}
22
-				mirrorURL, err := url.Parse(mirror)
23
-				if err != nil {
24
-					return nil, invalidParam(err)
25
-				}
26
-				// TODO(thaJeztah); this should all be memoized when loading the config. We're resolving mirrors and loading TLS config every time.
27
-				mirrorTLSConfig, err := newTLSConfig(ctx, mirrorURL.Host, s.config.isSecureIndex(mirrorURL.Host))
28
-				if err != nil {
29
-					return nil, err
30
-				}
31
-				endpoints = append(endpoints, APIEndpoint{
32
-					URL:       mirrorURL,
33
-					Mirror:    true,
34
-					TLSConfig: mirrorTLSConfig,
35
-				})
36
-			}
37
-		}
38
-		endpoints = append(endpoints, APIEndpoint{
39
-			URL:       DefaultV2Registry,
40
-			TLSConfig: tlsconfig.ServerDefault(),
41
-		})
42
-
43
-		return endpoints, nil
44
-	}
45
-
46
-	tlsConfig, err := newTLSConfig(ctx, hostname, s.config.isSecureIndex(hostname))
47
-	if err != nil {
48
-		return nil, err
49
-	}
50
-
51
-	endpoints = []APIEndpoint{
52
-		{
53
-			URL: &url.URL{
54
-				Scheme: "https",
55
-				Host:   hostname,
56
-			},
57
-			TLSConfig: tlsConfig,
58
-		},
59
-	}
60
-
61
-	if tlsConfig.InsecureSkipVerify {
62
-		endpoints = append(endpoints, APIEndpoint{
63
-			URL: &url.URL{
64
-				Scheme: "http",
65
-				Host:   hostname,
66
-			},
67
-			// used to check if supposed to be secure via InsecureSkipVerify
68
-			TLSConfig: tlsConfig,
69
-		})
70
-	}
71
-
72
-	return endpoints, nil
73
-}
74 1
deleted file mode 100644
... ...
@@ -1,13 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"github.com/distribution/reference"
5
-	"github.com/moby/moby/api/types/registry"
6
-)
7
-
8
-// RepositoryInfo describes a repository
9
-type RepositoryInfo struct {
10
-	Name reference.Named
11
-	// Index points to registry information
12
-	Index *registry.IndexInfo
13
-}
... ...
@@ -10,7 +10,7 @@ import (
10 10
 	"time"
11 11
 
12 12
 	"github.com/docker/docker/daemon/pkg/plugin"
13
-	registrypkg "github.com/docker/docker/registry"
13
+	registrypkg "github.com/docker/docker/daemon/pkg/registry"
14 14
 	"github.com/moby/go-archive"
15 15
 	"github.com/moby/moby/api/types"
16 16
 	"github.com/moby/moby/api/types/events"