github.com/lmars/docker@v1.6.0-rc2/registry/auth.go (about) 1 package registry 2 3 import ( 4 "crypto/tls" 5 "encoding/base64" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io/ioutil" 10 "net/http" 11 "os" 12 "path" 13 "strings" 14 "sync" 15 "time" 16 17 log "github.com/Sirupsen/logrus" 18 "github.com/docker/docker/utils" 19 ) 20 21 const ( 22 // Where we store the config file 23 CONFIGFILE = ".dockercfg" 24 ) 25 26 var ( 27 ErrConfigFileMissing = errors.New("The Auth config file is missing") 28 ) 29 30 type AuthConfig struct { 31 Username string `json:"username,omitempty"` 32 Password string `json:"password,omitempty"` 33 Auth string `json:"auth"` 34 Email string `json:"email"` 35 ServerAddress string `json:"serveraddress,omitempty"` 36 } 37 38 type ConfigFile struct { 39 Configs map[string]AuthConfig `json:"configs,omitempty"` 40 rootPath string 41 } 42 43 type RequestAuthorization struct { 44 authConfig *AuthConfig 45 registryEndpoint *Endpoint 46 resource string 47 scope string 48 actions []string 49 50 tokenLock sync.Mutex 51 tokenCache string 52 tokenExpiration time.Time 53 } 54 55 func NewRequestAuthorization(authConfig *AuthConfig, registryEndpoint *Endpoint, resource, scope string, actions []string) *RequestAuthorization { 56 return &RequestAuthorization{ 57 authConfig: authConfig, 58 registryEndpoint: registryEndpoint, 59 resource: resource, 60 scope: scope, 61 actions: actions, 62 } 63 } 64 65 func (auth *RequestAuthorization) getToken() (string, error) { 66 auth.tokenLock.Lock() 67 defer auth.tokenLock.Unlock() 68 now := time.Now() 69 if now.Before(auth.tokenExpiration) { 70 log.Debugf("Using cached token for %s", auth.authConfig.Username) 71 return auth.tokenCache, nil 72 } 73 74 tlsConfig := tls.Config{ 75 MinVersion: tls.VersionTLS10, 76 } 77 if !auth.registryEndpoint.IsSecure { 78 tlsConfig.InsecureSkipVerify = true 79 } 80 81 client := &http.Client{ 82 Transport: &http.Transport{ 83 DisableKeepAlives: true, 84 Proxy: http.ProxyFromEnvironment, 85 TLSClientConfig: &tlsConfig, 86 }, 87 CheckRedirect: AddRequiredHeadersToRedirectedRequests, 88 } 89 factory := HTTPRequestFactory(nil) 90 91 for _, challenge := range auth.registryEndpoint.AuthChallenges { 92 switch strings.ToLower(challenge.Scheme) { 93 case "basic": 94 // no token necessary 95 case "bearer": 96 log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, auth.authConfig.Username) 97 params := map[string]string{} 98 for k, v := range challenge.Parameters { 99 params[k] = v 100 } 101 params["scope"] = fmt.Sprintf("%s:%s:%s", auth.resource, auth.scope, strings.Join(auth.actions, ",")) 102 token, err := getToken(auth.authConfig.Username, auth.authConfig.Password, params, auth.registryEndpoint, client, factory) 103 if err != nil { 104 return "", err 105 } 106 auth.tokenCache = token 107 auth.tokenExpiration = now.Add(time.Minute) 108 109 return token, nil 110 default: 111 log.Infof("Unsupported auth scheme: %q", challenge.Scheme) 112 } 113 } 114 115 // Do not expire cache since there are no challenges which use a token 116 auth.tokenExpiration = time.Now().Add(time.Hour * 24) 117 118 return "", nil 119 } 120 121 func (auth *RequestAuthorization) Authorize(req *http.Request) error { 122 token, err := auth.getToken() 123 if err != nil { 124 return err 125 } 126 if token != "" { 127 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 128 } else if auth.authConfig.Username != "" && auth.authConfig.Password != "" { 129 req.SetBasicAuth(auth.authConfig.Username, auth.authConfig.Password) 130 } 131 return nil 132 } 133 134 // create a base64 encoded auth string to store in config 135 func encodeAuth(authConfig *AuthConfig) string { 136 authStr := authConfig.Username + ":" + authConfig.Password 137 msg := []byte(authStr) 138 encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg))) 139 base64.StdEncoding.Encode(encoded, msg) 140 return string(encoded) 141 } 142 143 // decode the auth string 144 func decodeAuth(authStr string) (string, string, error) { 145 decLen := base64.StdEncoding.DecodedLen(len(authStr)) 146 decoded := make([]byte, decLen) 147 authByte := []byte(authStr) 148 n, err := base64.StdEncoding.Decode(decoded, authByte) 149 if err != nil { 150 return "", "", err 151 } 152 if n > decLen { 153 return "", "", fmt.Errorf("Something went wrong decoding auth config") 154 } 155 arr := strings.SplitN(string(decoded), ":", 2) 156 if len(arr) != 2 { 157 return "", "", fmt.Errorf("Invalid auth configuration file") 158 } 159 password := strings.Trim(arr[1], "\x00") 160 return arr[0], password, nil 161 } 162 163 // load up the auth config information and return values 164 // FIXME: use the internal golang config parser 165 func LoadConfig(rootPath string) (*ConfigFile, error) { 166 configFile := ConfigFile{Configs: make(map[string]AuthConfig), rootPath: rootPath} 167 confFile := path.Join(rootPath, CONFIGFILE) 168 if _, err := os.Stat(confFile); err != nil { 169 return &configFile, nil //missing file is not an error 170 } 171 b, err := ioutil.ReadFile(confFile) 172 if err != nil { 173 return &configFile, err 174 } 175 176 if err := json.Unmarshal(b, &configFile.Configs); err != nil { 177 arr := strings.Split(string(b), "\n") 178 if len(arr) < 2 { 179 return &configFile, fmt.Errorf("The Auth config file is empty") 180 } 181 authConfig := AuthConfig{} 182 origAuth := strings.Split(arr[0], " = ") 183 if len(origAuth) != 2 { 184 return &configFile, fmt.Errorf("Invalid Auth config file") 185 } 186 authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1]) 187 if err != nil { 188 return &configFile, err 189 } 190 origEmail := strings.Split(arr[1], " = ") 191 if len(origEmail) != 2 { 192 return &configFile, fmt.Errorf("Invalid Auth config file") 193 } 194 authConfig.Email = origEmail[1] 195 authConfig.ServerAddress = IndexServerAddress() 196 // *TODO: Switch to using IndexServerName() instead? 197 configFile.Configs[IndexServerAddress()] = authConfig 198 } else { 199 for k, authConfig := range configFile.Configs { 200 authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth) 201 if err != nil { 202 return &configFile, err 203 } 204 authConfig.Auth = "" 205 authConfig.ServerAddress = k 206 configFile.Configs[k] = authConfig 207 } 208 } 209 return &configFile, nil 210 } 211 212 // save the auth config 213 func SaveConfig(configFile *ConfigFile) error { 214 confFile := path.Join(configFile.rootPath, CONFIGFILE) 215 if len(configFile.Configs) == 0 { 216 os.Remove(confFile) 217 return nil 218 } 219 220 configs := make(map[string]AuthConfig, len(configFile.Configs)) 221 for k, authConfig := range configFile.Configs { 222 authCopy := authConfig 223 224 authCopy.Auth = encodeAuth(&authCopy) 225 authCopy.Username = "" 226 authCopy.Password = "" 227 authCopy.ServerAddress = "" 228 configs[k] = authCopy 229 } 230 231 b, err := json.MarshalIndent(configs, "", "\t") 232 if err != nil { 233 return err 234 } 235 err = ioutil.WriteFile(confFile, b, 0600) 236 if err != nil { 237 return err 238 } 239 return nil 240 } 241 242 // Login tries to register/login to the registry server. 243 func Login(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) { 244 // Separates the v2 registry login logic from the v1 logic. 245 if registryEndpoint.Version == APIVersion2 { 246 return loginV2(authConfig, registryEndpoint, factory) 247 } 248 249 return loginV1(authConfig, registryEndpoint, factory) 250 } 251 252 // loginV1 tries to register/login to the v1 registry server. 253 func loginV1(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) { 254 var ( 255 status string 256 reqBody []byte 257 err error 258 client = &http.Client{ 259 Transport: &http.Transport{ 260 DisableKeepAlives: true, 261 Proxy: http.ProxyFromEnvironment, 262 }, 263 CheckRedirect: AddRequiredHeadersToRedirectedRequests, 264 } 265 reqStatusCode = 0 266 serverAddress = authConfig.ServerAddress 267 ) 268 269 log.Debugf("attempting v1 login to registry endpoint %s", registryEndpoint) 270 271 if serverAddress == "" { 272 return "", fmt.Errorf("Server Error: Server Address not set.") 273 } 274 275 loginAgainstOfficialIndex := serverAddress == IndexServerAddress() 276 277 // to avoid sending the server address to the server it should be removed before being marshalled 278 authCopy := *authConfig 279 authCopy.ServerAddress = "" 280 281 jsonBody, err := json.Marshal(authCopy) 282 if err != nil { 283 return "", fmt.Errorf("Config Error: %s", err) 284 } 285 286 // using `bytes.NewReader(jsonBody)` here causes the server to respond with a 411 status. 287 b := strings.NewReader(string(jsonBody)) 288 req1, err := http.Post(serverAddress+"users/", "application/json; charset=utf-8", b) 289 if err != nil { 290 return "", fmt.Errorf("Server Error: %s", err) 291 } 292 reqStatusCode = req1.StatusCode 293 defer req1.Body.Close() 294 reqBody, err = ioutil.ReadAll(req1.Body) 295 if err != nil { 296 return "", fmt.Errorf("Server Error: [%#v] %s", reqStatusCode, err) 297 } 298 299 if reqStatusCode == 201 { 300 if loginAgainstOfficialIndex { 301 status = "Account created. Please use the confirmation link we sent" + 302 " to your e-mail to activate it." 303 } else { 304 // *TODO: Use registry configuration to determine what this says, if anything? 305 status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it." 306 } 307 } else if reqStatusCode == 400 { 308 if string(reqBody) == "\"Username or email already exists\"" { 309 req, err := factory.NewRequest("GET", serverAddress+"users/", nil) 310 req.SetBasicAuth(authConfig.Username, authConfig.Password) 311 resp, err := client.Do(req) 312 if err != nil { 313 return "", err 314 } 315 defer resp.Body.Close() 316 body, err := ioutil.ReadAll(resp.Body) 317 if err != nil { 318 return "", err 319 } 320 if resp.StatusCode == 200 { 321 return "Login Succeeded", nil 322 } else if resp.StatusCode == 401 { 323 return "", fmt.Errorf("Wrong login/password, please try again") 324 } else if resp.StatusCode == 403 { 325 if loginAgainstOfficialIndex { 326 return "", fmt.Errorf("Login: Account is not Active. Please check your e-mail for a confirmation link.") 327 } 328 // *TODO: Use registry configuration to determine what this says, if anything? 329 return "", fmt.Errorf("Login: Account is not Active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress) 330 } 331 return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, resp.StatusCode, resp.Header) 332 } 333 return "", fmt.Errorf("Registration: %s", reqBody) 334 335 } else if reqStatusCode == 401 { 336 // This case would happen with private registries where /v1/users is 337 // protected, so people can use `docker login` as an auth check. 338 req, err := factory.NewRequest("GET", serverAddress+"users/", nil) 339 req.SetBasicAuth(authConfig.Username, authConfig.Password) 340 resp, err := client.Do(req) 341 if err != nil { 342 return "", err 343 } 344 defer resp.Body.Close() 345 body, err := ioutil.ReadAll(resp.Body) 346 if err != nil { 347 return "", err 348 } 349 if resp.StatusCode == 200 { 350 return "Login Succeeded", nil 351 } else if resp.StatusCode == 401 { 352 return "", fmt.Errorf("Wrong login/password, please try again") 353 } else { 354 return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, 355 resp.StatusCode, resp.Header) 356 } 357 } else { 358 return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody) 359 } 360 return status, nil 361 } 362 363 // loginV2 tries to login to the v2 registry server. The given registry endpoint has been 364 // pinged or setup with a list of authorization challenges. Each of these challenges are 365 // tried until one of them succeeds. Currently supported challenge schemes are: 366 // HTTP Basic Authorization 367 // Token Authorization with a separate token issuing server 368 // NOTE: the v2 logic does not attempt to create a user account if one doesn't exist. For 369 // now, users should create their account through other means like directly from a web page 370 // served by the v2 registry service provider. Whether this will be supported in the future 371 // is to be determined. 372 func loginV2(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) { 373 log.Debugf("attempting v2 login to registry endpoint %s", registryEndpoint) 374 375 tlsConfig := tls.Config{ 376 MinVersion: tls.VersionTLS10, 377 } 378 if !registryEndpoint.IsSecure { 379 tlsConfig.InsecureSkipVerify = true 380 } 381 382 client := &http.Client{ 383 Transport: &http.Transport{ 384 DisableKeepAlives: true, 385 Proxy: http.ProxyFromEnvironment, 386 TLSClientConfig: &tlsConfig, 387 }, 388 CheckRedirect: AddRequiredHeadersToRedirectedRequests, 389 } 390 391 var ( 392 err error 393 allErrors []error 394 ) 395 396 for _, challenge := range registryEndpoint.AuthChallenges { 397 log.Debugf("trying %q auth challenge with params %s", challenge.Scheme, challenge.Parameters) 398 399 switch strings.ToLower(challenge.Scheme) { 400 case "basic": 401 err = tryV2BasicAuthLogin(authConfig, challenge.Parameters, registryEndpoint, client, factory) 402 case "bearer": 403 err = tryV2TokenAuthLogin(authConfig, challenge.Parameters, registryEndpoint, client, factory) 404 default: 405 // Unsupported challenge types are explicitly skipped. 406 err = fmt.Errorf("unsupported auth scheme: %q", challenge.Scheme) 407 } 408 409 if err == nil { 410 return "Login Succeeded", nil 411 } 412 413 log.Debugf("error trying auth challenge %q: %s", challenge.Scheme, err) 414 415 allErrors = append(allErrors, err) 416 } 417 418 return "", fmt.Errorf("no successful auth challenge for %s - errors: %s", registryEndpoint, allErrors) 419 } 420 421 func tryV2BasicAuthLogin(authConfig *AuthConfig, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *utils.HTTPRequestFactory) error { 422 req, err := factory.NewRequest("GET", registryEndpoint.Path(""), nil) 423 if err != nil { 424 return err 425 } 426 427 req.SetBasicAuth(authConfig.Username, authConfig.Password) 428 429 resp, err := client.Do(req) 430 if err != nil { 431 return err 432 } 433 defer resp.Body.Close() 434 435 if resp.StatusCode != http.StatusOK { 436 return fmt.Errorf("basic auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode)) 437 } 438 439 return nil 440 } 441 442 func tryV2TokenAuthLogin(authConfig *AuthConfig, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *utils.HTTPRequestFactory) error { 443 token, err := getToken(authConfig.Username, authConfig.Password, params, registryEndpoint, client, factory) 444 if err != nil { 445 return err 446 } 447 448 req, err := factory.NewRequest("GET", registryEndpoint.Path(""), nil) 449 if err != nil { 450 return err 451 } 452 453 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 454 455 resp, err := client.Do(req) 456 if err != nil { 457 return err 458 } 459 defer resp.Body.Close() 460 461 if resp.StatusCode != http.StatusOK { 462 return fmt.Errorf("token auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode)) 463 } 464 465 return nil 466 } 467 468 // this method matches a auth configuration to a server address or a url 469 func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig { 470 configKey := index.GetAuthConfigKey() 471 // First try the happy case 472 if c, found := config.Configs[configKey]; found || index.Official { 473 return c 474 } 475 476 convertToHostname := func(url string) string { 477 stripped := url 478 if strings.HasPrefix(url, "http://") { 479 stripped = strings.Replace(url, "http://", "", 1) 480 } else if strings.HasPrefix(url, "https://") { 481 stripped = strings.Replace(url, "https://", "", 1) 482 } 483 484 nameParts := strings.SplitN(stripped, "/", 2) 485 486 return nameParts[0] 487 } 488 489 // Maybe they have a legacy config file, we will iterate the keys converting 490 // them to the new format and testing 491 for registry, config := range config.Configs { 492 if configKey == convertToHostname(registry) { 493 return config 494 } 495 } 496 497 // When all else fails, return an empty auth config 498 return AuthConfig{} 499 }