summaryrefslogtreecommitdiff
path: root/registry/auth.go
diff options
context:
space:
mode:
Diffstat (limited to 'registry/auth.go')
-rw-r--r--registry/auth.go290
1 files changed, 290 insertions, 0 deletions
diff --git a/registry/auth.go b/registry/auth.go
new file mode 100644
index 0000000000..4fdd51fda4
--- /dev/null
+++ b/registry/auth.go
@@ -0,0 +1,290 @@
+package registry
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "github.com/dotcloud/docker/utils"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path"
+ "strings"
+)
+
+// Where we store the config file
+const CONFIGFILE = ".dockercfg"
+
+// Only used for user auth + account creation
+const INDEXSERVER = "https://index.docker.io/v1/"
+
+//const INDEXSERVER = "https://indexstaging-docker.dotcloud.com/v1/"
+
+var (
+ ErrConfigFileMissing = errors.New("The Auth config file is missing")
+)
+
+type AuthConfig struct {
+ Username string `json:"username,omitempty"`
+ Password string `json:"password,omitempty"`
+ Auth string `json:"auth"`
+ Email string `json:"email"`
+ ServerAddress string `json:"serveraddress,omitempty"`
+}
+
+type ConfigFile struct {
+ Configs map[string]AuthConfig `json:"configs,omitempty"`
+ rootPath string
+}
+
+func IndexServerAddress() string {
+ return INDEXSERVER
+}
+
+// create a base64 encoded auth string to store in config
+func encodeAuth(authConfig *AuthConfig) string {
+ authStr := authConfig.Username + ":" + authConfig.Password
+ msg := []byte(authStr)
+ encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg)))
+ base64.StdEncoding.Encode(encoded, msg)
+ return string(encoded)
+}
+
+// decode the auth string
+func decodeAuth(authStr string) (string, string, error) {
+ decLen := base64.StdEncoding.DecodedLen(len(authStr))
+ decoded := make([]byte, decLen)
+ authByte := []byte(authStr)
+ n, err := base64.StdEncoding.Decode(decoded, authByte)
+ if err != nil {
+ return "", "", err
+ }
+ if n > decLen {
+ return "", "", fmt.Errorf("Something went wrong decoding auth config")
+ }
+ arr := strings.SplitN(string(decoded), ":", 2)
+ if len(arr) != 2 {
+ return "", "", fmt.Errorf("Invalid auth configuration file")
+ }
+ password := strings.Trim(arr[1], "\x00")
+ return arr[0], password, nil
+}
+
+// load up the auth config information and return values
+// FIXME: use the internal golang config parser
+func LoadConfig(rootPath string) (*ConfigFile, error) {
+ configFile := ConfigFile{Configs: make(map[string]AuthConfig), rootPath: rootPath}
+ confFile := path.Join(rootPath, CONFIGFILE)
+ if _, err := os.Stat(confFile); err != nil {
+ return &configFile, nil //missing file is not an error
+ }
+ b, err := ioutil.ReadFile(confFile)
+ if err != nil {
+ return &configFile, err
+ }
+
+ if err := json.Unmarshal(b, &configFile.Configs); err != nil {
+ arr := strings.Split(string(b), "\n")
+ if len(arr) < 2 {
+ return &configFile, fmt.Errorf("The Auth config file is empty")
+ }
+ authConfig := AuthConfig{}
+ origAuth := strings.Split(arr[0], " = ")
+ if len(origAuth) != 2 {
+ return &configFile, fmt.Errorf("Invalid Auth config file")
+ }
+ authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1])
+ if err != nil {
+ return &configFile, err
+ }
+ origEmail := strings.Split(arr[1], " = ")
+ if len(origEmail) != 2 {
+ return &configFile, fmt.Errorf("Invalid Auth config file")
+ }
+ authConfig.Email = origEmail[1]
+ authConfig.ServerAddress = IndexServerAddress()
+ configFile.Configs[IndexServerAddress()] = authConfig
+ } else {
+ for k, authConfig := range configFile.Configs {
+ authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth)
+ if err != nil {
+ return &configFile, err
+ }
+ authConfig.Auth = ""
+ configFile.Configs[k] = authConfig
+ authConfig.ServerAddress = k
+ }
+ }
+ return &configFile, nil
+}
+
+// save the auth config
+func SaveConfig(configFile *ConfigFile) error {
+ confFile := path.Join(configFile.rootPath, CONFIGFILE)
+ if len(configFile.Configs) == 0 {
+ os.Remove(confFile)
+ return nil
+ }
+
+ configs := make(map[string]AuthConfig, len(configFile.Configs))
+ for k, authConfig := range configFile.Configs {
+ authCopy := authConfig
+
+ authCopy.Auth = encodeAuth(&authCopy)
+ authCopy.Username = ""
+ authCopy.Password = ""
+ authCopy.ServerAddress = ""
+ configs[k] = authCopy
+ }
+
+ b, err := json.Marshal(configs)
+ if err != nil {
+ return err
+ }
+ err = ioutil.WriteFile(confFile, b, 0600)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// try to register/login to the registry server
+func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, error) {
+ var (
+ status string
+ reqBody []byte
+ err error
+ client = &http.Client{}
+ reqStatusCode = 0
+ serverAddress = authConfig.ServerAddress
+ )
+
+ if serverAddress == "" {
+ serverAddress = IndexServerAddress()
+ }
+
+ loginAgainstOfficialIndex := serverAddress == IndexServerAddress()
+
+ // to avoid sending the server address to the server it should be removed before being marshalled
+ authCopy := *authConfig
+ authCopy.ServerAddress = ""
+
+ jsonBody, err := json.Marshal(authCopy)
+ if err != nil {
+ return "", fmt.Errorf("Config Error: %s", err)
+ }
+
+ // using `bytes.NewReader(jsonBody)` here causes the server to respond with a 411 status.
+ b := strings.NewReader(string(jsonBody))
+ req1, err := http.Post(serverAddress+"users/", "application/json; charset=utf-8", b)
+ if err != nil {
+ return "", fmt.Errorf("Server Error: %s", err)
+ }
+ reqStatusCode = req1.StatusCode
+ defer req1.Body.Close()
+ reqBody, err = ioutil.ReadAll(req1.Body)
+ if err != nil {
+ return "", fmt.Errorf("Server Error: [%#v] %s", reqStatusCode, err)
+ }
+
+ if reqStatusCode == 201 {
+ if loginAgainstOfficialIndex {
+ status = "Account created. Please use the confirmation link we sent" +
+ " to your e-mail to activate it."
+ } else {
+ status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it."
+ }
+ } else if reqStatusCode == 400 {
+ if string(reqBody) == "\"Username or email already exists\"" {
+ req, err := factory.NewRequest("GET", serverAddress+"users/", nil)
+ req.SetBasicAuth(authConfig.Username, authConfig.Password)
+ resp, err := client.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+ if resp.StatusCode == 200 {
+ status = "Login Succeeded"
+ } else if resp.StatusCode == 401 {
+ return "", fmt.Errorf("Wrong login/password, please try again")
+ } else if resp.StatusCode == 403 {
+ if loginAgainstOfficialIndex {
+ return "", fmt.Errorf("Login: Account is not Active. Please check your e-mail for a confirmation link.")
+ }
+ return "", fmt.Errorf("Login: Account is not Active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress)
+ } else {
+ return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, resp.StatusCode, resp.Header)
+ }
+ } else {
+ return "", fmt.Errorf("Registration: %s", reqBody)
+ }
+ } else if reqStatusCode == 401 {
+ // This case would happen with private registries where /v1/users is
+ // protected, so people can use `docker login` as an auth check.
+ req, err := factory.NewRequest("GET", serverAddress+"users/", nil)
+ req.SetBasicAuth(authConfig.Username, authConfig.Password)
+ resp, err := client.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+ if resp.StatusCode == 200 {
+ status = "Login Succeeded"
+ } else if resp.StatusCode == 401 {
+ return "", fmt.Errorf("Wrong login/password, please try again")
+ } else {
+ return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
+ resp.StatusCode, resp.Header)
+ }
+ } else {
+ return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody)
+ }
+ return status, nil
+}
+
+// this method matches a auth configuration to a server address or a url
+func (config *ConfigFile) ResolveAuthConfig(hostname string) AuthConfig {
+ if hostname == IndexServerAddress() || len(hostname) == 0 {
+ // default to the index server
+ return config.Configs[IndexServerAddress()]
+ }
+
+ // First try the happy case
+ if c, found := config.Configs[hostname]; found {
+ return c
+ }
+
+ convertToHostname := func(url string) string {
+ stripped := url
+ if strings.HasPrefix(url, "http://") {
+ stripped = strings.Replace(url, "http://", "", 1)
+ } else if strings.HasPrefix(url, "https://") {
+ stripped = strings.Replace(url, "https://", "", 1)
+ }
+
+ nameParts := strings.SplitN(stripped, "/", 2)
+
+ return nameParts[0]
+ }
+
+ // Maybe they have a legacy config file, we will iterate the keys converting
+ // them to the new format and testing
+ normalizedHostename := convertToHostname(hostname)
+ for registry, config := range config.Configs {
+ if registryHostname := convertToHostname(registry); registryHostname == normalizedHostename {
+ return config
+ }
+ }
+
+ // When all else fails, return an empty auth config
+ return AuthConfig{}
+}