diff --git a/api.go b/api.go index 9e094231b5..39d242099d 100644 --- a/api.go +++ b/api.go @@ -2,6 +2,7 @@ package docker import ( "code.google.com/p/go.net/websocket" + "encoding/base64" "encoding/json" "fmt" "github.com/dotcloud/docker/auth" @@ -394,6 +395,16 @@ func postImagesCreate(srv *Server, version float64, w http.ResponseWriter, r *ht tag := r.Form.Get("tag") repo := r.Form.Get("repo") + authEncoded := r.Header.Get("X-Registry-Auth") + authConfig := &auth.AuthConfig{} + if authEncoded != "" { + authJson := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded)) + if err := json.NewDecoder(authJson).Decode(authConfig); err != nil { + // for a pull it is not an error if no auth was given + // to increase compatibility with the existing api it is defaulting to be empty + authConfig = &auth.AuthConfig{} + } + } if version > 1.0 { w.Header().Set("Content-Type", "application/json") } @@ -405,7 +416,7 @@ func postImagesCreate(srv *Server, version float64, w http.ResponseWriter, r *ht metaHeaders[k] = v } } - if err := srv.ImagePull(image, tag, w, sf, &auth.AuthConfig{}, metaHeaders, version > 1.3); err != nil { + if err := srv.ImagePull(image, tag, w, sf, authConfig, metaHeaders, version > 1.3); err != nil { if sf.Used() { w.Write(sf.FormatError(err)) return nil @@ -473,19 +484,32 @@ func postImagesInsert(srv *Server, version float64, w http.ResponseWriter, r *ht } func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - authConfig := &auth.AuthConfig{} metaHeaders := map[string][]string{} for k, v := range r.Header { if strings.HasPrefix(k, "X-Meta-") { metaHeaders[k] = v } } - if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil { - return err - } if err := parseForm(r); err != nil { return err } + authConfig := &auth.AuthConfig{} + + authEncoded := r.Header.Get("X-Registry-Auth") + if authEncoded != "" { + // the new format is to handle the authConfig as a header + authJson := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded)) + if err := json.NewDecoder(authJson).Decode(authConfig); err != nil { + // to increase compatibility to existing api it is defaulting to be empty + authConfig = &auth.AuthConfig{} + } + } else { + // the old format is supported for compatibility if there was no authConfig header + if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil { + return err + } + + } if vars == nil { return fmt.Errorf("Missing parameter") diff --git a/auth/auth.go b/auth/auth.go index 91314877c7..aff6de6dce 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -26,10 +26,11 @@ var ( ) type AuthConfig struct { - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - Auth string `json:"auth"` - Email string `json:"email"` + 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 { @@ -96,6 +97,7 @@ func LoadConfig(rootPath string) (*ConfigFile, error) { } origEmail := strings.Split(arr[1], " = ") authConfig.Email = origEmail[1] + authConfig.ServerAddress = IndexServerAddress() configFile.Configs[IndexServerAddress()] = authConfig } else { for k, authConfig := range configFile.Configs { @@ -105,6 +107,7 @@ func LoadConfig(rootPath string) (*ConfigFile, error) { } authConfig.Auth = "" configFile.Configs[k] = authConfig + authConfig.ServerAddress = k } } return &configFile, nil @@ -125,7 +128,7 @@ func SaveConfig(configFile *ConfigFile) error { authCopy.Auth = encodeAuth(&authCopy) authCopy.Username = "" authCopy.Password = "" - + authCopy.ServerAddress = "" configs[k] = authCopy } @@ -146,14 +149,26 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e reqStatusCode := 0 var status string var reqBody []byte - jsonBody, err := json.Marshal(authConfig) + + serverAddress := authConfig.ServerAddress + if serverAddress == "" { + serverAddress = IndexServerAddress() + } + + loginAgainstOfficialIndex := serverAddress == IndexServerAddress() + + // to avoid sending the server address to the server it should be removed before 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(IndexServerAddress()+"users/", "application/json; charset=utf-8", b) + req1, err := http.Post(serverAddress+"users/", "application/json; charset=utf-8", b) if err != nil { return "", fmt.Errorf("Server Error: %s", err) } @@ -165,14 +180,23 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e } if reqStatusCode == 201 { - status = "Account created. Please use the confirmation link we sent" + - " to your e-mail to activate it." + 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 == 403 { - return "", fmt.Errorf("Login: Your account hasn't been activated. " + - "Please check your e-mail for a confirmation link.") + if loginAgainstOfficialIndex { + return "", fmt.Errorf("Login: Your account hasn't been activated. " + + "Please check your e-mail for a confirmation link.") + } else { + return "", fmt.Errorf("Login: Your account hasn't been activated. " + + "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", IndexServerAddress()+"users/", nil) + req, err := factory.NewRequest("GET", serverAddress+"users/", nil) req.SetBasicAuth(authConfig.Username, authConfig.Password) resp, err := client.Do(req) if err != nil { @@ -199,3 +223,52 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e } return status, nil } + +// this method matches a auth configuration to a server address or a url +func (config *ConfigFile) ResolveAuthConfig(registry string) AuthConfig { + if registry == IndexServerAddress() || len(registry) == 0 { + // default to the index server + return config.Configs[IndexServerAddress()] + } + // if its not the index server there are three cases: + // + // 1. this is a full config url -> it should be used as is + // 2. it could be a full url, but with the wrong protocol + // 3. it can be the hostname optionally with a port + // + // as there is only one auth entry which is fully qualified we need to start + // parsing and matching + + swapProtocoll := func(url string) string { + if strings.HasPrefix(url, "http:") { + return strings.Replace(url, "http:", "https:", 1) + } + if strings.HasPrefix(url, "https:") { + return strings.Replace(url, "https:", "http:", 1) + } + return url + } + + resolveIgnoringProtocol := func(url string) AuthConfig { + if c, found := config.Configs[url]; found { + return c + } + registrySwappedProtocoll := swapProtocoll(url) + // now try to match with the different protocol + if c, found := config.Configs[registrySwappedProtocoll]; found { + return c + } + return AuthConfig{} + } + + // match both protocols as it could also be a server name like httpfoo + if strings.HasPrefix(registry, "http:") || strings.HasPrefix(registry, "https:") { + return resolveIgnoringProtocol(registry) + } + + url := "https://" + registry + if !strings.Contains(registry, "/") { + url = url + "/v1/" + } + return resolveIgnoringProtocol(url) +} diff --git a/commands.go b/commands.go index 5ca8309f4f..3dbea5cc16 100644 --- a/commands.go +++ b/commands.go @@ -4,10 +4,12 @@ import ( "archive/tar" "bufio" "bytes" + "encoding/base64" "encoding/json" "flag" "fmt" "github.com/dotcloud/docker/auth" + "github.com/dotcloud/docker/registry" "github.com/dotcloud/docker/term" "github.com/dotcloud/docker/utils" "io" @@ -92,6 +94,7 @@ func (cli *DockerCli) CmdHelp(args ...string) error { {"login", "Register or Login to the docker registry server"}, {"logs", "Fetch the logs of a container"}, {"port", "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT"}, + {"top", "Lookup the running processes of a container"}, {"ps", "List containers"}, {"pull", "Pull an image or a repository from the docker registry server"}, {"push", "Push an image or a repository to the docker registry server"}, @@ -103,7 +106,6 @@ func (cli *DockerCli) CmdHelp(args ...string) error { {"start", "Start a stopped container"}, {"stop", "Stop a running container"}, {"tag", "Tag an image into a repository"}, - {"top", "Lookup the running processes of a container"}, {"version", "Show the docker version information"}, {"wait", "Block until a container stops, then print its exit code"}, } { @@ -127,7 +129,7 @@ func (cli *DockerCli) CmdInsert(args ...string) error { v.Set("url", cmd.Arg(1)) v.Set("path", cmd.Arg(2)) - if err := cli.stream("POST", "/images/"+cmd.Arg(0)+"/insert?"+v.Encode(), nil, cli.out); err != nil { + if err := cli.stream("POST", "/images/"+cmd.Arg(0)+"/insert?"+v.Encode(), nil, cli.out, nil); err != nil { return err } return nil @@ -188,10 +190,8 @@ func (cli *DockerCli) CmdBuild(args ...string) error { } else if utils.IsURL(cmd.Arg(0)) || utils.IsGIT(cmd.Arg(0)) { isRemote = true } else { - if fi, err := os.Stat(cmd.Arg(0)); err != nil { + if _, err := os.Stat(cmd.Arg(0)); err != nil { return err - } else if !fi.IsDir() { - return fmt.Errorf("\"%s\" is not a path or URL. Please provide a path to a directory containing a Dockerfile.", cmd.Arg(0)) } context, err = Tar(cmd.Arg(0), Uncompressed) } @@ -255,7 +255,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error { // 'docker login': login / register a user to registry service. func (cli *DockerCli) CmdLogin(args ...string) error { - cmd := Subcmd("login", "[OPTIONS]", "Register or Login to the docker registry server") + cmd := Subcmd("login", "[OPTIONS] [SERVER]", "Register or Login to a docker registry server, if no server is specified \""+auth.IndexServerAddress()+"\" is the default.") var username, password, email string @@ -263,10 +263,17 @@ func (cli *DockerCli) CmdLogin(args ...string) error { cmd.StringVar(&password, "p", "", "password") cmd.StringVar(&email, "e", "", "email") err := cmd.Parse(args) - if err != nil { return nil } + serverAddress := auth.IndexServerAddress() + if len(cmd.Args()) > 0 { + serverAddress, err = registry.ExpandAndVerifyRegistryUrl(cmd.Arg(0)) + if err != nil { + return err + } + fmt.Fprintf(cli.out, "Login against server at %s\n", serverAddress) + } promptDefault := func(prompt string, configDefault string) { if configDefault == "" { @@ -299,19 +306,16 @@ func (cli *DockerCli) CmdLogin(args ...string) error { username = authconfig.Username } } - if username != authconfig.Username { if password == "" { oldState, _ := term.SaveState(cli.terminalFd) fmt.Fprintf(cli.out, "Password: ") - term.DisableEcho(cli.terminalFd, oldState) password = readInput(cli.in, cli.out) fmt.Fprint(cli.out, "\n") term.RestoreTerminal(cli.terminalFd, oldState) - if password == "" { return fmt.Errorf("Error : Password Required") } @@ -328,15 +332,15 @@ func (cli *DockerCli) CmdLogin(args ...string) error { password = authconfig.Password email = authconfig.Email } - authconfig.Username = username authconfig.Password = password authconfig.Email = email - cli.configFile.Configs[auth.IndexServerAddress()] = authconfig + authconfig.ServerAddress = serverAddress + cli.configFile.Configs[serverAddress] = authconfig - body, statusCode, err := cli.call("POST", "/auth", cli.configFile.Configs[auth.IndexServerAddress()]) + body, statusCode, err := cli.call("POST", "/auth", cli.configFile.Configs[serverAddress]) if statusCode == 401 { - delete(cli.configFile.Configs, auth.IndexServerAddress()) + delete(cli.configFile.Configs, serverAddress) auth.SaveConfig(cli.configFile) return err } @@ -792,7 +796,7 @@ func (cli *DockerCli) CmdImport(args ...string) error { v.Set("tag", tag) v.Set("fromSrc", src) - err := cli.stream("POST", "/images/create?"+v.Encode(), cli.in, cli.out) + err := cli.stream("POST", "/images/create?"+v.Encode(), cli.in, cli.out, nil) if err != nil { return err } @@ -813,6 +817,13 @@ func (cli *DockerCli) CmdPush(args ...string) error { cli.LoadConfigFile() + // Resolve the Repository name from fqn to endpoint + name + endpoint, _, err := registry.ResolveRepositoryName(name) + if err != nil { + return err + } + // Resolve the Auth config relevant for this server + authConfig := cli.configFile.ResolveAuthConfig(endpoint) // If we're not using a custom registry, we know the restrictions // applied to repository names and can warn the user in advance. // Custom repositories can have different rules, and we must also @@ -826,22 +837,28 @@ func (cli *DockerCli) CmdPush(args ...string) error { } v := url.Values{} - push := func() error { - buf, err := json.Marshal(cli.configFile.Configs[auth.IndexServerAddress()]) + push := func(authConfig auth.AuthConfig) error { + buf, err := json.Marshal(authConfig) if err != nil { return err } + registryAuthHeader := []string{ + base64.URLEncoding.EncodeToString(buf), + } - return cli.stream("POST", "/images/"+name+"/push?"+v.Encode(), bytes.NewBuffer(buf), cli.out) + return cli.stream("POST", "/images/"+name+"/push?"+v.Encode(), nil, cli.out, map[string][]string{ + "X-Registry-Auth": registryAuthHeader, + }) } - if err := push(); err != nil { - if err.Error() == "Authentication is required." { + if err := push(authConfig); err != nil { + if err.Error() == registry.ErrLoginRequired.Error() { fmt.Fprintln(cli.out, "\nPlease login prior to push:") - if err := cli.CmdLogin(""); err != nil { + if err := cli.CmdLogin(endpoint); err != nil { return err } - return push() + authConfig := cli.configFile.ResolveAuthConfig(endpoint) + return push(authConfig) } return err } @@ -865,11 +882,43 @@ func (cli *DockerCli) CmdPull(args ...string) error { *tag = parsedTag } + // Resolve the Repository name from fqn to endpoint + name + endpoint, _, err := registry.ResolveRepositoryName(remote) + if err != nil { + return err + } + + cli.LoadConfigFile() + + // Resolve the Auth config relevant for this server + authConfig := cli.configFile.ResolveAuthConfig(endpoint) v := url.Values{} v.Set("fromImage", remote) v.Set("tag", *tag) - if err := cli.stream("POST", "/images/create?"+v.Encode(), nil, cli.out); err != nil { + pull := func(authConfig auth.AuthConfig) error { + buf, err := json.Marshal(authConfig) + if err != nil { + return err + } + registryAuthHeader := []string{ + base64.URLEncoding.EncodeToString(buf), + } + + return cli.stream("POST", "/images/create?"+v.Encode(), nil, cli.out, map[string][]string{ + "X-Registry-Auth": registryAuthHeader, + }) + } + + if err := pull(authConfig); err != nil { + if err.Error() == registry.ErrLoginRequired.Error() { + fmt.Fprintln(cli.out, "\nPlease login prior to push:") + if err := cli.CmdLogin(endpoint); err != nil { + return err + } + authConfig := cli.configFile.ResolveAuthConfig(endpoint) + return pull(authConfig) + } return err } @@ -1116,7 +1165,7 @@ func (cli *DockerCli) CmdEvents(args ...string) error { v.Set("since", *since) } - if err := cli.stream("GET", "/events?"+v.Encode(), nil, cli.out); err != nil { + if err := cli.stream("GET", "/events?"+v.Encode(), nil, cli.out, nil); err != nil { return err } return nil @@ -1133,7 +1182,7 @@ func (cli *DockerCli) CmdExport(args ...string) error { return nil } - if err := cli.stream("GET", "/containers/"+cmd.Arg(0)+"/export", nil, cli.out); err != nil { + if err := cli.stream("GET", "/containers/"+cmd.Arg(0)+"/export", nil, cli.out, nil); err != nil { return err } return nil @@ -1405,7 +1454,30 @@ func (cli *DockerCli) CmdRun(args ...string) error { repos, tag := utils.ParseRepositoryTag(config.Image) v.Set("fromImage", repos) v.Set("tag", tag) - err = cli.stream("POST", "/images/create?"+v.Encode(), nil, cli.err) + + // Resolve the Repository name from fqn to endpoint + name + var endpoint string + endpoint, _, err = registry.ResolveRepositoryName(repos) + if err != nil { + return err + } + + // Load the auth config file, to be able to pull the image + cli.LoadConfigFile() + + // Resolve the Auth config relevant for this server + authConfig := cli.configFile.ResolveAuthConfig(endpoint) + buf, err := json.Marshal(authConfig) + if err != nil { + return err + } + + registryAuthHeader := []string{ + base64.URLEncoding.EncodeToString(buf), + } + err = cli.stream("POST", "/images/create?"+v.Encode(), nil, cli.err, map[string][]string{ + "X-Registry-Auth": registryAuthHeader, + }) if err != nil { return err } @@ -1582,7 +1654,7 @@ func (cli *DockerCli) call(method, path string, data interface{}) ([]byte, int, return body, resp.StatusCode, nil } -func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer) error { +func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer, headers map[string][]string) error { if (method == "POST" || method == "PUT") && in == nil { in = bytes.NewReader([]byte{}) } @@ -1595,6 +1667,13 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer) e if method == "POST" { req.Header.Set("Content-Type", "plain/text") } + + if headers != nil { + for k, v := range headers { + req.Header[k] = v + } + } + dial, err := net.Dial(cli.proto, cli.addr) if err != nil { if strings.Contains(err.Error(), "connection refused") { diff --git a/docs/sources/api/docker_remote_api_v1.4.rst b/docs/sources/api/docker_remote_api_v1.4.rst index 3ab5cdee0a..df355c78a5 100644 --- a/docs/sources/api/docker_remote_api_v1.4.rst +++ b/docs/sources/api/docker_remote_api_v1.4.rst @@ -991,7 +991,8 @@ Check auth configuration { "username":"hannibal", "password:"xxxx", - "email":"hannibal@a-team.com" + "email":"hannibal@a-team.com", + "serveraddress":"https://index.docker.io/v1/" } **Example response**: diff --git a/docs/sources/commandline/command/login.rst b/docs/sources/commandline/command/login.rst index 57ecaeb00e..46f354d6be 100644 --- a/docs/sources/commandline/command/login.rst +++ b/docs/sources/commandline/command/login.rst @@ -8,10 +8,17 @@ :: - Usage: docker login [OPTIONS] + Usage: docker login [OPTIONS] [SERVER] Register or Login to the docker registry server -e="": email -p="": password -u="": username + + If you want to login to a private registry you can + specify this by adding the server name. + + example: + docker login localhost:8080 + diff --git a/registry/registry.go b/registry/registry.go index 759652f074..f24e0b3502 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -22,6 +22,7 @@ import ( var ( ErrAlreadyExists = errors.New("Image already exists") ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")") + ErrLoginRequired = errors.New("Authentication is required.") ) func pingRegistryEndpoint(endpoint string) error { @@ -102,17 +103,38 @@ func ResolveRepositoryName(reposName string) (string, string, error) { if err := validateRepositoryName(reposName); err != nil { return "", "", err } + endpoint, err := ExpandAndVerifyRegistryUrl(hostname) + if err != nil { + return "", "", err + } + return endpoint, reposName, err +} + +// this method expands the registry name as used in the prefix of a repo +// to a full url. if it already is a url, there will be no change. +// The registry is pinged to test if it http or https +func ExpandAndVerifyRegistryUrl(hostname string) (string, error) { + if strings.HasPrefix(hostname, "http:") || strings.HasPrefix(hostname, "https:") { + // if there is no slash after https:// (8 characters) then we have no path in the url + if strings.LastIndex(hostname, "/") < 9 { + // there is no path given. Expand with default path + hostname = hostname + "/v1/" + } + if err := pingRegistryEndpoint(hostname); err != nil { + return "", errors.New("Invalid Registry endpoint: " + err.Error()) + } + return hostname, nil + } endpoint := fmt.Sprintf("https://%s/v1/", hostname) if err := pingRegistryEndpoint(endpoint); err != nil { utils.Debugf("Registry %s does not work (%s), falling back to http", endpoint, err) endpoint = fmt.Sprintf("http://%s/v1/", hostname) if err = pingRegistryEndpoint(endpoint); err != nil { //TODO: triggering highland build can be done there without "failing" - return "", "", errors.New("Invalid Registry endpoint: " + err.Error()) + return "", errors.New("Invalid Registry endpoint: " + err.Error()) } } - err := validateRepositoryName(reposName) - return endpoint, reposName, err + return endpoint, nil } func doWithCookies(c *http.Client, req *http.Request) (*http.Response, error) { @@ -139,6 +161,9 @@ func (r *Registry) GetRemoteHistory(imgID, registry string, token []string) ([]s req.Header.Set("Authorization", "Token "+strings.Join(token, ", ")) res, err := doWithCookies(r.client, req) if err != nil || res.StatusCode != 200 { + if res.StatusCode == 401 { + return nil, ErrLoginRequired + } if res != nil { return nil, utils.NewHTTPRequestError(fmt.Sprintf("Internal server error: %d trying to fetch remote history for %s", res.StatusCode, imgID), res) } @@ -282,7 +307,7 @@ func (r *Registry) GetRepositoryData(indexEp, remote string) (*RepositoryData, e } defer res.Body.Close() if res.StatusCode == 401 { - return nil, utils.NewHTTPRequestError(fmt.Sprintf("Please login first (HTTP code %d)", res.StatusCode), res) + return nil, ErrLoginRequired } // TODO: Right now we're ignoring checksums in the response body. // In the future, we need to use them to check image validity. diff --git a/server.go b/server.go index 386f0e27b4..d6352c82a4 100644 --- a/server.go +++ b/server.go @@ -655,6 +655,9 @@ func (srv *Server) ImagePull(localName string, tag string, out io.Writer, sf *ut out = utils.NewWriteFlusher(out) err = srv.pullRepository(r, out, localName, remoteName, tag, endpoint, sf, parallel) + if err == registry.ErrLoginRequired { + return err + } if err != nil { if err := srv.pullImage(r, out, remoteName, endpoint, nil, sf); err != nil { return err