github.com/tsuna/docker@v1.7.0-rc3/registry/auth.go (about) 1 package registry 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "strings" 9 "sync" 10 "time" 11 12 "github.com/Sirupsen/logrus" 13 "github.com/docker/docker/cliconfig" 14 ) 15 16 type RequestAuthorization struct { 17 authConfig *cliconfig.AuthConfig 18 registryEndpoint *Endpoint 19 resource string 20 scope string 21 actions []string 22 23 tokenLock sync.Mutex 24 tokenCache string 25 tokenExpiration time.Time 26 } 27 28 func NewRequestAuthorization(authConfig *cliconfig.AuthConfig, registryEndpoint *Endpoint, resource, scope string, actions []string) *RequestAuthorization { 29 return &RequestAuthorization{ 30 authConfig: authConfig, 31 registryEndpoint: registryEndpoint, 32 resource: resource, 33 scope: scope, 34 actions: actions, 35 } 36 } 37 38 func (auth *RequestAuthorization) getToken() (string, error) { 39 auth.tokenLock.Lock() 40 defer auth.tokenLock.Unlock() 41 now := time.Now() 42 if now.Before(auth.tokenExpiration) { 43 logrus.Debugf("Using cached token for %s", auth.authConfig.Username) 44 return auth.tokenCache, nil 45 } 46 47 for _, challenge := range auth.registryEndpoint.AuthChallenges { 48 switch strings.ToLower(challenge.Scheme) { 49 case "basic": 50 // no token necessary 51 case "bearer": 52 logrus.Debugf("Getting bearer token with %s for %s", challenge.Parameters, auth.authConfig.Username) 53 params := map[string]string{} 54 for k, v := range challenge.Parameters { 55 params[k] = v 56 } 57 params["scope"] = fmt.Sprintf("%s:%s:%s", auth.resource, auth.scope, strings.Join(auth.actions, ",")) 58 token, err := getToken(auth.authConfig.Username, auth.authConfig.Password, params, auth.registryEndpoint) 59 if err != nil { 60 return "", err 61 } 62 auth.tokenCache = token 63 auth.tokenExpiration = now.Add(time.Minute) 64 65 return token, nil 66 default: 67 logrus.Infof("Unsupported auth scheme: %q", challenge.Scheme) 68 } 69 } 70 71 // Do not expire cache since there are no challenges which use a token 72 auth.tokenExpiration = time.Now().Add(time.Hour * 24) 73 74 return "", nil 75 } 76 77 func (auth *RequestAuthorization) Authorize(req *http.Request) error { 78 token, err := auth.getToken() 79 if err != nil { 80 return err 81 } 82 if token != "" { 83 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 84 } else if auth.authConfig.Username != "" && auth.authConfig.Password != "" { 85 req.SetBasicAuth(auth.authConfig.Username, auth.authConfig.Password) 86 } 87 return nil 88 } 89 90 // Login tries to register/login to the registry server. 91 func Login(authConfig *cliconfig.AuthConfig, registryEndpoint *Endpoint) (string, error) { 92 // Separates the v2 registry login logic from the v1 logic. 93 if registryEndpoint.Version == APIVersion2 { 94 return loginV2(authConfig, registryEndpoint) 95 } 96 return loginV1(authConfig, registryEndpoint) 97 } 98 99 // loginV1 tries to register/login to the v1 registry server. 100 func loginV1(authConfig *cliconfig.AuthConfig, registryEndpoint *Endpoint) (string, error) { 101 var ( 102 status string 103 reqBody []byte 104 err error 105 reqStatusCode = 0 106 serverAddress = authConfig.ServerAddress 107 ) 108 109 logrus.Debugf("attempting v1 login to registry endpoint %s", registryEndpoint) 110 111 if serverAddress == "" { 112 return "", fmt.Errorf("Server Error: Server Address not set.") 113 } 114 115 loginAgainstOfficialIndex := serverAddress == IndexServerAddress() 116 117 // to avoid sending the server address to the server it should be removed before being marshalled 118 authCopy := *authConfig 119 authCopy.ServerAddress = "" 120 121 jsonBody, err := json.Marshal(authCopy) 122 if err != nil { 123 return "", fmt.Errorf("Config Error: %s", err) 124 } 125 126 // using `bytes.NewReader(jsonBody)` here causes the server to respond with a 411 status. 127 b := strings.NewReader(string(jsonBody)) 128 req1, err := registryEndpoint.client.Post(serverAddress+"users/", "application/json; charset=utf-8", b) 129 if err != nil { 130 return "", fmt.Errorf("Server Error: %s", err) 131 } 132 reqStatusCode = req1.StatusCode 133 defer req1.Body.Close() 134 reqBody, err = ioutil.ReadAll(req1.Body) 135 if err != nil { 136 return "", fmt.Errorf("Server Error: [%#v] %s", reqStatusCode, err) 137 } 138 139 if reqStatusCode == 201 { 140 if loginAgainstOfficialIndex { 141 status = "Account created. Please use the confirmation link we sent" + 142 " to your e-mail to activate it." 143 } else { 144 // *TODO: Use registry configuration to determine what this says, if anything? 145 status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it." 146 } 147 } else if reqStatusCode == 400 { 148 if string(reqBody) == "\"Username or email already exists\"" { 149 req, err := http.NewRequest("GET", serverAddress+"users/", nil) 150 req.SetBasicAuth(authConfig.Username, authConfig.Password) 151 resp, err := registryEndpoint.client.Do(req) 152 if err != nil { 153 return "", err 154 } 155 defer resp.Body.Close() 156 body, err := ioutil.ReadAll(resp.Body) 157 if err != nil { 158 return "", err 159 } 160 if resp.StatusCode == 200 { 161 return "Login Succeeded", nil 162 } else if resp.StatusCode == 401 { 163 return "", fmt.Errorf("Wrong login/password, please try again") 164 } else if resp.StatusCode == 403 { 165 if loginAgainstOfficialIndex { 166 return "", fmt.Errorf("Login: Account is not Active. Please check your e-mail for a confirmation link.") 167 } 168 // *TODO: Use registry configuration to determine what this says, if anything? 169 return "", fmt.Errorf("Login: Account is not Active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress) 170 } 171 return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, resp.StatusCode, resp.Header) 172 } 173 return "", fmt.Errorf("Registration: %s", reqBody) 174 175 } else if reqStatusCode == 401 { 176 // This case would happen with private registries where /v1/users is 177 // protected, so people can use `docker login` as an auth check. 178 req, err := http.NewRequest("GET", serverAddress+"users/", nil) 179 req.SetBasicAuth(authConfig.Username, authConfig.Password) 180 resp, err := registryEndpoint.client.Do(req) 181 if err != nil { 182 return "", err 183 } 184 defer resp.Body.Close() 185 body, err := ioutil.ReadAll(resp.Body) 186 if err != nil { 187 return "", err 188 } 189 if resp.StatusCode == 200 { 190 return "Login Succeeded", nil 191 } else if resp.StatusCode == 401 { 192 return "", fmt.Errorf("Wrong login/password, please try again") 193 } else { 194 return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, 195 resp.StatusCode, resp.Header) 196 } 197 } else { 198 return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody) 199 } 200 return status, nil 201 } 202 203 // loginV2 tries to login to the v2 registry server. The given registry endpoint has been 204 // pinged or setup with a list of authorization challenges. Each of these challenges are 205 // tried until one of them succeeds. Currently supported challenge schemes are: 206 // HTTP Basic Authorization 207 // Token Authorization with a separate token issuing server 208 // NOTE: the v2 logic does not attempt to create a user account if one doesn't exist. For 209 // now, users should create their account through other means like directly from a web page 210 // served by the v2 registry service provider. Whether this will be supported in the future 211 // is to be determined. 212 func loginV2(authConfig *cliconfig.AuthConfig, registryEndpoint *Endpoint) (string, error) { 213 logrus.Debugf("attempting v2 login to registry endpoint %s", registryEndpoint) 214 var ( 215 err error 216 allErrors []error 217 ) 218 219 for _, challenge := range registryEndpoint.AuthChallenges { 220 logrus.Debugf("trying %q auth challenge with params %s", challenge.Scheme, challenge.Parameters) 221 222 switch strings.ToLower(challenge.Scheme) { 223 case "basic": 224 err = tryV2BasicAuthLogin(authConfig, challenge.Parameters, registryEndpoint) 225 case "bearer": 226 err = tryV2TokenAuthLogin(authConfig, challenge.Parameters, registryEndpoint) 227 default: 228 // Unsupported challenge types are explicitly skipped. 229 err = fmt.Errorf("unsupported auth scheme: %q", challenge.Scheme) 230 } 231 232 if err == nil { 233 return "Login Succeeded", nil 234 } 235 236 logrus.Debugf("error trying auth challenge %q: %s", challenge.Scheme, err) 237 238 allErrors = append(allErrors, err) 239 } 240 241 return "", fmt.Errorf("no successful auth challenge for %s - errors: %s", registryEndpoint, allErrors) 242 } 243 244 func tryV2BasicAuthLogin(authConfig *cliconfig.AuthConfig, params map[string]string, registryEndpoint *Endpoint) error { 245 req, err := http.NewRequest("GET", registryEndpoint.Path(""), nil) 246 if err != nil { 247 return err 248 } 249 250 req.SetBasicAuth(authConfig.Username, authConfig.Password) 251 252 resp, err := registryEndpoint.client.Do(req) 253 if err != nil { 254 return err 255 } 256 defer resp.Body.Close() 257 258 if resp.StatusCode != http.StatusOK { 259 return fmt.Errorf("basic auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode)) 260 } 261 262 return nil 263 } 264 265 func tryV2TokenAuthLogin(authConfig *cliconfig.AuthConfig, params map[string]string, registryEndpoint *Endpoint) error { 266 token, err := getToken(authConfig.Username, authConfig.Password, params, registryEndpoint) 267 if err != nil { 268 return err 269 } 270 271 req, err := http.NewRequest("GET", registryEndpoint.Path(""), nil) 272 if err != nil { 273 return err 274 } 275 276 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 277 278 resp, err := registryEndpoint.client.Do(req) 279 if err != nil { 280 return err 281 } 282 defer resp.Body.Close() 283 284 if resp.StatusCode != http.StatusOK { 285 return fmt.Errorf("token auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode)) 286 } 287 288 return nil 289 } 290 291 // this method matches a auth configuration to a server address or a url 292 func ResolveAuthConfig(config *cliconfig.ConfigFile, index *IndexInfo) cliconfig.AuthConfig { 293 configKey := index.GetAuthConfigKey() 294 // First try the happy case 295 if c, found := config.AuthConfigs[configKey]; found || index.Official { 296 return c 297 } 298 299 convertToHostname := func(url string) string { 300 stripped := url 301 if strings.HasPrefix(url, "http://") { 302 stripped = strings.Replace(url, "http://", "", 1) 303 } else if strings.HasPrefix(url, "https://") { 304 stripped = strings.Replace(url, "https://", "", 1) 305 } 306 307 nameParts := strings.SplitN(stripped, "/", 2) 308 309 return nameParts[0] 310 } 311 312 // Maybe they have a legacy config file, we will iterate the keys converting 313 // them to the new format and testing 314 for registry, ac := range config.AuthConfigs { 315 if configKey == convertToHostname(registry) { 316 return ac 317 } 318 } 319 320 // When all else fails, return an empty auth config 321 return cliconfig.AuthConfig{} 322 }