github.com/titanous/docker@v1.4.1/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 "net/url" 11 "os" 12 "path" 13 "strings" 14 15 "github.com/docker/docker/utils" 16 ) 17 18 const ( 19 // Where we store the config file 20 CONFIGFILE = ".dockercfg" 21 22 // Only used for user auth + account creation 23 INDEXSERVER = "https://index.docker.io/v1/" 24 REGISTRYSERVER = "https://registry-1.docker.io/v1/" 25 26 // INDEXSERVER = "https://registry-stage.hub.docker.com/v1/" 27 ) 28 29 var ( 30 ErrConfigFileMissing = errors.New("The Auth config file is missing") 31 IndexServerURL *url.URL 32 ) 33 34 func init() { 35 url, err := url.Parse(INDEXSERVER) 36 if err != nil { 37 panic(err) 38 } 39 IndexServerURL = url 40 } 41 42 type AuthConfig struct { 43 Username string `json:"username,omitempty"` 44 Password string `json:"password,omitempty"` 45 Auth string `json:"auth"` 46 Email string `json:"email"` 47 ServerAddress string `json:"serveraddress,omitempty"` 48 } 49 50 type ConfigFile struct { 51 Configs map[string]AuthConfig `json:"configs,omitempty"` 52 rootPath string 53 } 54 55 func IndexServerAddress() string { 56 return INDEXSERVER 57 } 58 59 // create a base64 encoded auth string to store in config 60 func encodeAuth(authConfig *AuthConfig) string { 61 authStr := authConfig.Username + ":" + authConfig.Password 62 msg := []byte(authStr) 63 encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg))) 64 base64.StdEncoding.Encode(encoded, msg) 65 return string(encoded) 66 } 67 68 // decode the auth string 69 func decodeAuth(authStr string) (string, string, error) { 70 decLen := base64.StdEncoding.DecodedLen(len(authStr)) 71 decoded := make([]byte, decLen) 72 authByte := []byte(authStr) 73 n, err := base64.StdEncoding.Decode(decoded, authByte) 74 if err != nil { 75 return "", "", err 76 } 77 if n > decLen { 78 return "", "", fmt.Errorf("Something went wrong decoding auth config") 79 } 80 arr := strings.SplitN(string(decoded), ":", 2) 81 if len(arr) != 2 { 82 return "", "", fmt.Errorf("Invalid auth configuration file") 83 } 84 password := strings.Trim(arr[1], "\x00") 85 return arr[0], password, nil 86 } 87 88 // load up the auth config information and return values 89 // FIXME: use the internal golang config parser 90 func LoadConfig(rootPath string) (*ConfigFile, error) { 91 configFile := ConfigFile{Configs: make(map[string]AuthConfig), rootPath: rootPath} 92 confFile := path.Join(rootPath, CONFIGFILE) 93 if _, err := os.Stat(confFile); err != nil { 94 return &configFile, nil //missing file is not an error 95 } 96 b, err := ioutil.ReadFile(confFile) 97 if err != nil { 98 return &configFile, err 99 } 100 101 if err := json.Unmarshal(b, &configFile.Configs); err != nil { 102 arr := strings.Split(string(b), "\n") 103 if len(arr) < 2 { 104 return &configFile, fmt.Errorf("The Auth config file is empty") 105 } 106 authConfig := AuthConfig{} 107 origAuth := strings.Split(arr[0], " = ") 108 if len(origAuth) != 2 { 109 return &configFile, fmt.Errorf("Invalid Auth config file") 110 } 111 authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1]) 112 if err != nil { 113 return &configFile, err 114 } 115 origEmail := strings.Split(arr[1], " = ") 116 if len(origEmail) != 2 { 117 return &configFile, fmt.Errorf("Invalid Auth config file") 118 } 119 authConfig.Email = origEmail[1] 120 authConfig.ServerAddress = IndexServerAddress() 121 configFile.Configs[IndexServerAddress()] = authConfig 122 } else { 123 for k, authConfig := range configFile.Configs { 124 authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth) 125 if err != nil { 126 return &configFile, err 127 } 128 authConfig.Auth = "" 129 authConfig.ServerAddress = k 130 configFile.Configs[k] = authConfig 131 } 132 } 133 return &configFile, nil 134 } 135 136 // save the auth config 137 func SaveConfig(configFile *ConfigFile) error { 138 confFile := path.Join(configFile.rootPath, CONFIGFILE) 139 if len(configFile.Configs) == 0 { 140 os.Remove(confFile) 141 return nil 142 } 143 144 configs := make(map[string]AuthConfig, len(configFile.Configs)) 145 for k, authConfig := range configFile.Configs { 146 authCopy := authConfig 147 148 authCopy.Auth = encodeAuth(&authCopy) 149 authCopy.Username = "" 150 authCopy.Password = "" 151 authCopy.ServerAddress = "" 152 configs[k] = authCopy 153 } 154 155 b, err := json.Marshal(configs) 156 if err != nil { 157 return err 158 } 159 err = ioutil.WriteFile(confFile, b, 0600) 160 if err != nil { 161 return err 162 } 163 return nil 164 } 165 166 // try to register/login to the registry server 167 func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, error) { 168 var ( 169 status string 170 reqBody []byte 171 err error 172 client = &http.Client{ 173 Transport: &http.Transport{ 174 DisableKeepAlives: true, 175 Proxy: http.ProxyFromEnvironment, 176 }, 177 CheckRedirect: AddRequiredHeadersToRedirectedRequests, 178 } 179 reqStatusCode = 0 180 serverAddress = authConfig.ServerAddress 181 ) 182 183 if serverAddress == "" { 184 serverAddress = IndexServerAddress() 185 } 186 187 loginAgainstOfficialIndex := serverAddress == IndexServerAddress() 188 189 // to avoid sending the server address to the server it should be removed before being marshalled 190 authCopy := *authConfig 191 authCopy.ServerAddress = "" 192 193 jsonBody, err := json.Marshal(authCopy) 194 if err != nil { 195 return "", fmt.Errorf("Config Error: %s", err) 196 } 197 198 // using `bytes.NewReader(jsonBody)` here causes the server to respond with a 411 status. 199 b := strings.NewReader(string(jsonBody)) 200 req1, err := http.Post(serverAddress+"users/", "application/json; charset=utf-8", b) 201 if err != nil { 202 return "", fmt.Errorf("Server Error: %s", err) 203 } 204 reqStatusCode = req1.StatusCode 205 defer req1.Body.Close() 206 reqBody, err = ioutil.ReadAll(req1.Body) 207 if err != nil { 208 return "", fmt.Errorf("Server Error: [%#v] %s", reqStatusCode, err) 209 } 210 211 if reqStatusCode == 201 { 212 if loginAgainstOfficialIndex { 213 status = "Account created. Please use the confirmation link we sent" + 214 " to your e-mail to activate it." 215 } else { 216 status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it." 217 } 218 } else if reqStatusCode == 400 { 219 if string(reqBody) == "\"Username or email already exists\"" { 220 req, err := factory.NewRequest("GET", serverAddress+"users/", nil) 221 req.SetBasicAuth(authConfig.Username, authConfig.Password) 222 resp, err := client.Do(req) 223 if err != nil { 224 return "", err 225 } 226 defer resp.Body.Close() 227 body, err := ioutil.ReadAll(resp.Body) 228 if err != nil { 229 return "", err 230 } 231 if resp.StatusCode == 200 { 232 return "Login Succeeded", nil 233 } else if resp.StatusCode == 401 { 234 return "", fmt.Errorf("Wrong login/password, please try again") 235 } else if resp.StatusCode == 403 { 236 if loginAgainstOfficialIndex { 237 return "", fmt.Errorf("Login: Account is not Active. Please check your e-mail for a confirmation link.") 238 } 239 return "", fmt.Errorf("Login: Account is not Active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress) 240 } 241 return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, resp.StatusCode, resp.Header) 242 } 243 return "", fmt.Errorf("Registration: %s", reqBody) 244 245 } else if reqStatusCode == 401 { 246 // This case would happen with private registries where /v1/users is 247 // protected, so people can use `docker login` as an auth check. 248 req, err := factory.NewRequest("GET", serverAddress+"users/", nil) 249 req.SetBasicAuth(authConfig.Username, authConfig.Password) 250 resp, err := client.Do(req) 251 if err != nil { 252 return "", err 253 } 254 defer resp.Body.Close() 255 body, err := ioutil.ReadAll(resp.Body) 256 if err != nil { 257 return "", err 258 } 259 if resp.StatusCode == 200 { 260 return "Login Succeeded", nil 261 } else if resp.StatusCode == 401 { 262 return "", fmt.Errorf("Wrong login/password, please try again") 263 } else { 264 return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, 265 resp.StatusCode, resp.Header) 266 } 267 } else { 268 return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody) 269 } 270 return status, nil 271 } 272 273 // this method matches a auth configuration to a server address or a url 274 func (config *ConfigFile) ResolveAuthConfig(hostname string) AuthConfig { 275 if hostname == IndexServerAddress() || len(hostname) == 0 { 276 // default to the index server 277 return config.Configs[IndexServerAddress()] 278 } 279 280 // First try the happy case 281 if c, found := config.Configs[hostname]; found { 282 return c 283 } 284 285 convertToHostname := func(url string) string { 286 stripped := url 287 if strings.HasPrefix(url, "http://") { 288 stripped = strings.Replace(url, "http://", "", 1) 289 } else if strings.HasPrefix(url, "https://") { 290 stripped = strings.Replace(url, "https://", "", 1) 291 } 292 293 nameParts := strings.SplitN(stripped, "/", 2) 294 295 return nameParts[0] 296 } 297 298 // Maybe they have a legacy config file, we will iterate the keys converting 299 // them to the new format and testing 300 normalizedHostename := convertToHostname(hostname) 301 for registry, config := range config.Configs { 302 if registryHostname := convertToHostname(registry); registryHostname == normalizedHostename { 303 return config 304 } 305 } 306 307 // When all else fails, return an empty auth config 308 return AuthConfig{} 309 }