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