From be20f3c518cb1587a2c731a1d6d91bdedcbf1dc9 Mon Sep 17 00:00:00 2001 From: Ken Cochrane Date: Thu, 14 Mar 2013 17:43:59 -0700 Subject: [PATCH 01/10] 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.") From 5e79c4394a51300cde3fbbc39e2a40e10d512e66 Mon Sep 17 00:00:00 2001 From: Ken Cochrane Date: Thu, 14 Mar 2013 17:53:10 -0700 Subject: [PATCH 02/10] changed scanf's with fscanf's, so it works better for remote --- commands/commands.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/commands/commands.go b/commands/commands.go index c16136412d..681c04961e 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -84,16 +84,16 @@ func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout io.Writer, args ...strin } fmt.Fprint(stdout, "Username (", authConfig.Username, "): ") - fmt.Scanf("%s", &username) + fmt.Fscanf(stdin, "%s", &username) if username == "" { username = authConfig.Username } if username != authConfig.Username { fmt.Fprint(stdout, "Password: ") - fmt.Scanf("%s", &password) + fmt.Fscanf(stdin, "%s", &password) fmt.Fprint(stdout, "Email (", authConfig.Email, "): ") - fmt.Scanf("%s", &email) + fmt.Fscanf(stdin, "%s", &email) if email == "" { email = authConfig.Email } From f1cf5074f568c4b853d6ac91c358babdbb12a544 Mon Sep 17 00:00:00 2001 From: Ken Cochrane Date: Thu, 14 Mar 2013 18:43:02 -0700 Subject: [PATCH 03/10] cleaned up the code a little --- auth/auth.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index e2db6d857d..822c1f8540 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -92,14 +92,14 @@ func Login(authConfig AuthConfig) (string, error) { 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 { + if err != nil { return "", err } + + reqBody, _ = ioutil.ReadAll(req1.Body) + reqStatusCode = req1.StatusCode + req1.Body.Close() + if reqStatusCode == 201 { status = "Account Created\n" storeConfig = true @@ -121,12 +121,16 @@ func Login(authConfig AuthConfig) (string, error) { status = "Login Succeeded\n" storeConfig = true } else { - storeConfig = false - status = fmt.Sprintf("Error: %s\n", body) + status = fmt.Sprintf("Login Error: %s\n", body) + return "", errors.New(status) } } else { - status = fmt.Sprintf("Error: %s\n", reqBody) + status = fmt.Sprintf("Registration Error: %s\n", reqBody) + return "", errors.New(status) } + } else { + status = fmt.Sprintf("Error: %s : %s \n", reqStatusCode, reqBody) + return "", errors.New(status) } if storeConfig { authStr := EncodeAuth(authConfig) From 9b94d89b066d23d6fbadc10eebd52584e93309e4 Mon Sep 17 00:00:00 2001 From: Ken Cochrane Date: Thu, 14 Mar 2013 19:43:42 -0700 Subject: [PATCH 04/10] added more message changes --- auth/auth.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index 822c1f8540..954a7b262e 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -96,9 +96,9 @@ func Login(authConfig AuthConfig) (string, error) { return "", err } + defer req1.Body.Close() reqBody, _ = ioutil.ReadAll(req1.Body) reqStatusCode = req1.StatusCode - req1.Body.Close() if reqStatusCode == 201 { status = "Account Created\n" @@ -121,15 +121,15 @@ func Login(authConfig AuthConfig) (string, error) { status = "Login Succeeded\n" storeConfig = true } else { - status = fmt.Sprintf("Login Error: %s\n", body) + status = fmt.Sprintf("Login: %s\n", body) return "", errors.New(status) } } else { - status = fmt.Sprintf("Registration Error: %s\n", reqBody) + status = fmt.Sprintf("Registration: %s\n", string(reqBody)) return "", errors.New(status) } } else { - status = fmt.Sprintf("Error: %s : %s \n", reqStatusCode, reqBody) + status = fmt.Sprintf("[%s] : %s \n", reqStatusCode, string(reqBody)) return "", errors.New(status) } if storeConfig { From d94a5b7d05a66c6856d719fbdd8770f7b154503b Mon Sep 17 00:00:00 2001 From: Ken Cochrane Date: Thu, 14 Mar 2013 20:21:03 -0700 Subject: [PATCH 05/10] only show status message if there is one to show --- commands.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/commands.go b/commands.go index 4bf3f8a9df..09a6789216 100644 --- a/commands.go +++ b/commands.go @@ -105,7 +105,9 @@ func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout io.Writer, args ...strin if err != nil { fmt.Fprintf(stdout, "Error : %s\n", err) } - fmt.Fprintf(stdout, status) + if status != "" { + fmt.Fprintf(stdout, status) + } return nil } From 7ec6a311f81289dac6305d5c91be37a97b2499a5 Mon Sep 17 00:00:00 2001 From: Ken Cochrane Date: Thu, 14 Mar 2013 20:23:45 -0700 Subject: [PATCH 06/10] Removed the extra newline char from the messages --- auth/auth.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index 954a7b262e..4224a686d5 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -121,15 +121,15 @@ func Login(authConfig AuthConfig) (string, error) { status = "Login Succeeded\n" storeConfig = true } else { - status = fmt.Sprintf("Login: %s\n", body) + status = fmt.Sprintf("Login: %s", body) return "", errors.New(status) } } else { - status = fmt.Sprintf("Registration: %s\n", string(reqBody)) + status = fmt.Sprintf("Registration: %s", string(reqBody)) return "", errors.New(status) } } else { - status = fmt.Sprintf("[%s] : %s \n", reqStatusCode, string(reqBody)) + status = fmt.Sprintf("[%s] : %s", reqStatusCode, string(reqBody)) return "", errors.New(status) } if storeConfig { From 878ae25980c21b6678fa30ba09b426e7710e1b14 Mon Sep 17 00:00:00 2001 From: Ken Cochrane Date: Fri, 15 Mar 2013 07:49:27 -0700 Subject: [PATCH 07/10] made sure password was required, fixed docker help issue with login prompt --- commands.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/commands.go b/commands.go index 09a6789216..ffc000ebf6 100644 --- a/commands.go +++ b/commands.go @@ -74,6 +74,10 @@ func (srv *Server) Help() string { // 'docker login': login / register a user to registry service. func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout io.Writer, args ...string) error { + cmd := rcli.Subcmd(stdout, "login", "", "Register or Login to the docker registry server") + if err := cmd.Parse(args); err != nil { + return nil + } var username string var password string var email string @@ -91,6 +95,10 @@ func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout io.Writer, args ...strin fmt.Fprint(stdout, "Password: ") fmt.Fscanf(stdin, "%s", &password) + if password == "" { + return errors.New("Error : Password Required\n") + } + fmt.Fprint(stdout, "Email (", authConfig.Email, "): ") fmt.Fscanf(stdin, "%s", &email) if email == "" { From 0a35db8fd061247b1db20fde92237f02a4e6882c Mon Sep 17 00:00:00 2001 From: Ken Cochrane Date: Fri, 15 Mar 2013 14:41:55 -0700 Subject: [PATCH 08/10] added more debugging/ error catching --- auth/auth.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index 4224a686d5..8f34904247 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -89,16 +89,26 @@ func Login(authConfig AuthConfig) (string, error) { reqStatusCode := 0 var status string var reqBody []byte - jsonBody, _ := json.Marshal(authConfig) + jsonBody, err := json.Marshal(authConfig) + if err != nil { + errMsg = fmt.Sprintf("Config Error: %s", err) + return "", errors.New(errMsg) + } + b := strings.NewReader(string(jsonBody)) req1, err := http.Post(REGISTRY_SERVER+"/v1/users", "application/json; charset=utf-8", b) if err != nil { - return "", err + errMsg = fmt.Sprintf("Server Error: %s", err) + return "", errors.New(errMsg) } - defer req1.Body.Close() - reqBody, _ = ioutil.ReadAll(req1.Body) reqStatusCode = req1.StatusCode + defer req1.Body.Close() + reqBody, err = ioutil.ReadAll(req1.Body) + if err != nil { + errMsg = fmt.Sprintf("Server Error: [%s] %s", reqStatusCode, err) + return "", errors.New(errMsg) + } if reqStatusCode == 201 { status = "Account Created\n" From 27ad71e025538b903a3eac8ac1e4eea654346b4d Mon Sep 17 00:00:00 2001 From: Ken Cochrane Date: Fri, 15 Mar 2013 14:48:42 -0700 Subject: [PATCH 09/10] fixed missing varible, error: --- auth/auth.go | 1 + 1 file changed, 1 insertion(+) diff --git a/auth/auth.go b/auth/auth.go index 8f34904247..1f8d112d3d 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -88,6 +88,7 @@ func Login(authConfig AuthConfig) (string, error) { storeConfig := false reqStatusCode := 0 var status string + var errMsg string var reqBody []byte jsonBody, err := json.Marshal(authConfig) if err != nil { From c4640689affc84c5a975059c5fca27a8b22bd1d0 Mon Sep 17 00:00:00 2001 From: Ken Cochrane Date: Fri, 15 Mar 2013 15:04:36 -0700 Subject: [PATCH 10/10] added better error message --- auth/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/auth.go b/auth/auth.go index 1f8d112d3d..ae0e71b111 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -107,7 +107,7 @@ func Login(authConfig AuthConfig) (string, error) { defer req1.Body.Close() reqBody, err = ioutil.ReadAll(req1.Body) if err != nil { - errMsg = fmt.Sprintf("Server Error: [%s] %s", reqStatusCode, err) + errMsg = fmt.Sprintf("Server Error: [%#v] %s", reqStatusCode, err) return "", errors.New(errMsg) }