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