1package registry // import "github.com/docker/docker/registry"
2
3import (
4	"net/http"
5	"net/url"
6	"strings"
7	"time"
8
9	"github.com/docker/distribution/registry/client/auth"
10	"github.com/docker/distribution/registry/client/auth/challenge"
11	"github.com/docker/distribution/registry/client/transport"
12	"github.com/docker/docker/api/types"
13	registrytypes "github.com/docker/docker/api/types/registry"
14	"github.com/pkg/errors"
15	"github.com/sirupsen/logrus"
16)
17
18const (
19	// AuthClientID is used the ClientID used for the token server
20	AuthClientID = "docker"
21)
22
23type loginCredentialStore struct {
24	authConfig *types.AuthConfig
25}
26
27func (lcs loginCredentialStore) Basic(*url.URL) (string, string) {
28	return lcs.authConfig.Username, lcs.authConfig.Password
29}
30
31func (lcs loginCredentialStore) RefreshToken(*url.URL, string) string {
32	return lcs.authConfig.IdentityToken
33}
34
35func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token string) {
36	lcs.authConfig.IdentityToken = token
37}
38
39type staticCredentialStore struct {
40	auth *types.AuthConfig
41}
42
43// NewStaticCredentialStore returns a credential store
44// which always returns the same credential values.
45func NewStaticCredentialStore(auth *types.AuthConfig) auth.CredentialStore {
46	return staticCredentialStore{
47		auth: auth,
48	}
49}
50
51func (scs staticCredentialStore) Basic(*url.URL) (string, string) {
52	if scs.auth == nil {
53		return "", ""
54	}
55	return scs.auth.Username, scs.auth.Password
56}
57
58func (scs staticCredentialStore) RefreshToken(*url.URL, string) string {
59	if scs.auth == nil {
60		return ""
61	}
62	return scs.auth.IdentityToken
63}
64
65func (scs staticCredentialStore) SetRefreshToken(*url.URL, string, string) {
66}
67
68type fallbackError struct {
69	err error
70}
71
72func (err fallbackError) Error() string {
73	return err.err.Error()
74}
75
76// loginV2 tries to login to the v2 registry server. The given registry
77// endpoint will be pinged to get authorization challenges. These challenges
78// will be used to authenticate against the registry to validate credentials.
79func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent string) (string, string, error) {
80	var (
81		endpointStr          = strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
82		modifiers            = Headers(userAgent, nil)
83		authTransport        = transport.NewTransport(NewTransport(endpoint.TLSConfig), modifiers...)
84		credentialAuthConfig = *authConfig
85		creds                = loginCredentialStore{authConfig: &credentialAuthConfig}
86	)
87
88	logrus.Debugf("attempting v2 login to registry endpoint %s", endpointStr)
89
90	loginClient, foundV2, err := v2AuthHTTPClient(endpoint.URL, authTransport, modifiers, creds, nil)
91	if err != nil {
92		return "", "", err
93	}
94
95	req, err := http.NewRequest(http.MethodGet, endpointStr, nil)
96	if err != nil {
97		if !foundV2 {
98			err = fallbackError{err: err}
99		}
100		return "", "", err
101	}
102
103	resp, err := loginClient.Do(req)
104	if err != nil {
105		err = translateV2AuthError(err)
106		if !foundV2 {
107			err = fallbackError{err: err}
108		}
109
110		return "", "", err
111	}
112	defer resp.Body.Close()
113
114	if resp.StatusCode == http.StatusOK {
115		return "Login Succeeded", credentialAuthConfig.IdentityToken, nil
116	}
117
118	// TODO(dmcgowan): Attempt to further interpret result, status code and error code string
119	err = errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
120	if !foundV2 {
121		err = fallbackError{err: err}
122	}
123	return "", "", err
124}
125
126func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, bool, error) {
127	challengeManager, foundV2, err := PingV2Registry(endpoint, authTransport)
128	if err != nil {
129		if !foundV2 {
130			err = fallbackError{err: err}
131		}
132		return nil, foundV2, err
133	}
134
135	tokenHandlerOptions := auth.TokenHandlerOptions{
136		Transport:     authTransport,
137		Credentials:   creds,
138		OfflineAccess: true,
139		ClientID:      AuthClientID,
140		Scopes:        scopes,
141	}
142	tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions)
143	basicHandler := auth.NewBasicHandler(creds)
144	modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
145	tr := transport.NewTransport(authTransport, modifiers...)
146
147	return &http.Client{
148		Transport: tr,
149		Timeout:   15 * time.Second,
150	}, foundV2, nil
151
152}
153
154// ConvertToHostname converts a registry url which has http|https prepended
155// to just an hostname.
156func ConvertToHostname(url string) string {
157	stripped := url
158	if strings.HasPrefix(url, "http://") {
159		stripped = strings.TrimPrefix(url, "http://")
160	} else if strings.HasPrefix(url, "https://") {
161		stripped = strings.TrimPrefix(url, "https://")
162	}
163
164	nameParts := strings.SplitN(stripped, "/", 2)
165
166	return nameParts[0]
167}
168
169// ResolveAuthConfig matches an auth configuration to a server address or a URL
170func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registrytypes.IndexInfo) types.AuthConfig {
171	configKey := GetAuthConfigKey(index)
172	// First try the happy case
173	if c, found := authConfigs[configKey]; found || index.Official {
174		return c
175	}
176
177	// Maybe they have a legacy config file, we will iterate the keys converting
178	// them to the new format and testing
179	for registry, ac := range authConfigs {
180		if configKey == ConvertToHostname(registry) {
181			return ac
182		}
183	}
184
185	// When all else fails, return an empty auth config
186	return types.AuthConfig{}
187}
188
189// PingResponseError is used when the response from a ping
190// was received but invalid.
191type PingResponseError struct {
192	Err error
193}
194
195func (err PingResponseError) Error() string {
196	return err.Err.Error()
197}
198
199// PingV2Registry attempts to ping a v2 registry and on success return a
200// challenge manager for the supported authentication types and
201// whether v2 was confirmed by the response. If a response is received but
202// cannot be interpreted a PingResponseError will be returned.
203func PingV2Registry(endpoint *url.URL, transport http.RoundTripper) (challenge.Manager, bool, error) {
204	var (
205		foundV2   = false
206		v2Version = auth.APIVersion{
207			Type:    "registry",
208			Version: "2.0",
209		}
210	)
211
212	pingClient := &http.Client{
213		Transport: transport,
214		Timeout:   15 * time.Second,
215	}
216	endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/"
217	req, err := http.NewRequest(http.MethodGet, endpointStr, nil)
218	if err != nil {
219		return nil, false, err
220	}
221	resp, err := pingClient.Do(req)
222	if err != nil {
223		return nil, false, err
224	}
225	defer resp.Body.Close()
226
227	versions := auth.APIVersions(resp, DefaultRegistryVersionHeader)
228	for _, pingVersion := range versions {
229		if pingVersion == v2Version {
230			// The version header indicates we're definitely
231			// talking to a v2 registry. So don't allow future
232			// fallbacks to the v1 protocol.
233
234			foundV2 = true
235			break
236		}
237	}
238
239	challengeManager := challenge.NewSimpleManager()
240	if err := challengeManager.AddResponse(resp); err != nil {
241		return nil, foundV2, PingResponseError{
242			Err: err,
243		}
244	}
245
246	return challengeManager, foundV2, nil
247}
248