From be20f3c518cb1587a2c731a1d6d91bdedcbf1dc9 Mon Sep 17 00:00:00 2001 From: Ken Cochrane Date: Thu, 14 Mar 2013 17:43:59 -0700 Subject: [PATCH] added ability to login/register to the docker registry, via the docker login command --- auth/auth.go | 136 +++++++++++++++++++++++++++++++++++++++++++ auth/auth_test.go | 23 ++++++++ commands/commands.go | 39 +++++++++++++ 3 files changed, 198 insertions(+) create mode 100644 auth/auth.go create mode 100644 auth/auth_test.go diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000000..e2db6d857d --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,136 @@ +package auth + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "strings" +) + +// Where we store the config file +const CONFIGFILE = "/var/lib/docker/.dockercfg" + +// the registry server we want to login against +const REGISTRY_SERVER = "http://registry.docker.io" + +type AuthConfig struct { + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email"` +} + +// 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) (AuthConfig, 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 AuthConfig{}, err + } + if n > decLen { + return AuthConfig{}, errors.New("something went wrong decoding auth config") + } + arr := strings.Split(string(decoded), ":") + password := strings.Trim(arr[1], "\x00") + return AuthConfig{Username: arr[0], Password: password}, nil + +} + +// load up the auth config information and return values +func LoadConfig() (AuthConfig, error) { + if _, err := os.Stat(CONFIGFILE); err == nil { + b, err := ioutil.ReadFile(CONFIGFILE) + if err != nil { + return AuthConfig{}, err + } + arr := strings.Split(string(b), "\n") + orig_auth := strings.Split(arr[0], " = ") + orig_email := strings.Split(arr[1], " = ") + authConfig, err := DecodeAuth(orig_auth[1]) + if err != nil { + return AuthConfig{}, err + } + authConfig.Email = orig_email[1] + return authConfig, nil + } else { + return AuthConfig{}, nil + } + return AuthConfig{}, nil +} + +// save the auth config +func saveConfig(authStr string, email string) error { + lines := "auth = " + authStr + "\n" + "email = " + email + "\n" + b := []byte(lines) + err := ioutil.WriteFile(CONFIGFILE, b, 0600) + if err != nil { + return err + } + return nil +} + +// try to register/login to the registry server +func Login(authConfig AuthConfig) (string, error) { + storeConfig := false + reqStatusCode := 0 + var status string + var reqBody []byte + jsonBody, _ := json.Marshal(authConfig) + b := strings.NewReader(string(jsonBody)) + req1, err := http.Post(REGISTRY_SERVER+"/v1/users", "application/json; charset=utf-8", b) + if err == nil { + body, _ := ioutil.ReadAll(req1.Body) + reqStatusCode = req1.StatusCode + reqBody = body + req1.Body.Close() + } else { + return "", err + } + if reqStatusCode == 201 { + status = "Account Created\n" + storeConfig = true + } else if reqStatusCode == 400 { + if string(reqBody) == "Username or email already exist" { + client := &http.Client{} + req, err := http.NewRequest("GET", REGISTRY_SERVER+"/v1/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\n" + storeConfig = true + } else { + storeConfig = false + status = fmt.Sprintf("Error: %s\n", body) + } + } else { + status = fmt.Sprintf("Error: %s\n", reqBody) + } + } + if storeConfig { + authStr := EncodeAuth(authConfig) + saveConfig(authStr, authConfig.Email) + } + return status, nil +} diff --git a/auth/auth_test.go b/auth/auth_test.go new file mode 100644 index 0000000000..d1650668e7 --- /dev/null +++ b/auth/auth_test.go @@ -0,0 +1,23 @@ +package auth + +import ( + "testing" +) + +func TestEncodeAuth(t *testing.T) { + newAuthConfig := AuthConfig{Username: "ken", Password: "test", Email: "test@example.com"} + authStr := EncodeAuth(newAuthConfig) + decAuthConfig, err := DecodeAuth(authStr) + if err != nil { + t.Fatal(err) + } + if newAuthConfig.Username != decAuthConfig.Username { + t.Fatal("Encode Username doesn't match decoded Username") + } + if newAuthConfig.Password != decAuthConfig.Password { + t.Fatal("Encode Password doesn't match decoded Password") + } + if authStr != "a2VuOnRlc3Q=" { + t.Fatal("AuthString encoding isn't correct.") + } +} diff --git a/commands/commands.go b/commands/commands.go index 781fdeafe0..c16136412d 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "github.com/dotcloud/docker" + "github.com/dotcloud/docker/auth" "github.com/dotcloud/docker/fs" "github.com/dotcloud/docker/future" "github.com/dotcloud/docker/rcli" @@ -47,6 +48,7 @@ func (srv *Server) Help() string { {"inspect", "Return low-level information on a container"}, {"kill", "Kill a running container"}, {"layers", "(debug only) List filesystem layers"}, + {"login", "Register or Login to the docker registry server"}, {"logs", "Fetch the logs of a container"}, {"ls", "List the contents of a container's directory"}, {"mirror", "(debug only) (No documentation available)"}, @@ -71,6 +73,43 @@ func (srv *Server) Help() string { return help } +// 'docker login': login / register a user to registry service. +func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout io.Writer, args ...string) error { + var username string + var password string + var email string + authConfig, err := auth.LoadConfig() + if err != nil { + fmt.Fprintf(stdout, "Error : %s\n", err) + } + + fmt.Fprint(stdout, "Username (", authConfig.Username, "): ") + fmt.Scanf("%s", &username) + if username == "" { + username = authConfig.Username + } + if username != authConfig.Username { + fmt.Fprint(stdout, "Password: ") + fmt.Scanf("%s", &password) + + fmt.Fprint(stdout, "Email (", authConfig.Email, "): ") + fmt.Scanf("%s", &email) + if email == "" { + email = authConfig.Email + } + } else { + password = authConfig.Password + email = authConfig.Email + } + newAuthConfig := auth.AuthConfig{Username: username, Password: password, Email: email} + status, err := auth.Login(newAuthConfig) + if err != nil { + fmt.Fprintf(stdout, "Error : %s\n", err) + } + fmt.Fprintf(stdout, status) + return nil +} + // 'docker wait': block until a container stops func (srv *Server) CmdWait(stdin io.ReadCloser, stdout io.Writer, args ...string) error { cmd := rcli.Subcmd(stdout, "wait", "[OPTIONS] NAME", "Block until a container stops, then print its exit code.")