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