Browse code

Login against private registry

To improve the use of docker with a private registry the login
command is extended with a parameter for the server address.

While implementing i noticed that two problems hindered authentication to a
private registry:

1. the resolve of the authentication did not match during push
because the looked up key was for example localhost:8080 but
the stored one would have been https://localhost:8080

Besides The lookup needs to still work if the https->http fallback
is used

2. During pull of an image no authentication is sent, which
means all repositories are expected to be private.

These points are fixed now. The changes are implemented in
a way to be compatible to existing behavior both in the
API as also with the private registry.

Update:

- login does not require the full url any more, you can login
to the repository prefix:

example:
docker logon localhost:8080

Fixed corner corner cases:

- When login is done during pull and push the registry endpoint is used and
not the central index

- When Remote sends a 401 during pull, it is now correctly delegating to
CmdLogin

- After a Login is done pull and push are using the newly entered login data,
and not the previous ones. This one seems to be also broken in master, too.

- Auth config is now transfered in a parameter instead of the body when
/images/create is called.

Marco Hennings authored on 2013/09/04 03:45:49
Showing 7 changed files
... ...
@@ -3,6 +3,7 @@ package docker
3 3
 import (
4 4
 	"code.google.com/p/go.net/websocket"
5 5
 	"encoding/json"
6
+	"encoding/base64"
6 7
 	"fmt"
7 8
 	"github.com/dotcloud/docker/auth"
8 9
 	"github.com/dotcloud/docker/utils"
... ...
@@ -394,6 +395,16 @@ func postImagesCreate(srv *Server, version float64, w http.ResponseWriter, r *ht
394 394
 	tag := r.Form.Get("tag")
395 395
 	repo := r.Form.Get("repo")
396 396
 
397
+	authEncoded := r.Form.Get("authConfig")
398
+	authConfig := &auth.AuthConfig{}
399
+	if authEncoded != "" {
400
+		authJson := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
401
+		if err := json.NewDecoder(authJson).Decode(authConfig); err != nil {
402
+			// for a pull it is not an error if no auth was given
403
+			// to increase compatibilit to existing api it is defaulting to be empty
404
+			authConfig = &auth.AuthConfig{}
405
+		}
406
+	}
397 407
 	if version > 1.0 {
398 408
 		w.Header().Set("Content-Type", "application/json")
399 409
 	}
... ...
@@ -405,7 +416,7 @@ func postImagesCreate(srv *Server, version float64, w http.ResponseWriter, r *ht
405 405
 				metaHeaders[k] = v
406 406
 			}
407 407
 		}
408
-		if err := srv.ImagePull(image, tag, w, sf, &auth.AuthConfig{}, metaHeaders, version > 1.3); err != nil {
408
+		if err := srv.ImagePull(image, tag, w, sf, authConfig, metaHeaders, version > 1.3); err != nil {
409 409
 			if sf.Used() {
410 410
 				w.Write(sf.FormatError(err))
411 411
 				return nil
... ...
@@ -26,10 +26,11 @@ var (
26 26
 )
27 27
 
28 28
 type AuthConfig struct {
29
-	Username string `json:"username,omitempty"`
30
-	Password string `json:"password,omitempty"`
31
-	Auth     string `json:"auth"`
32
-	Email    string `json:"email"`
29
+	Username      string `json:"username,omitempty"`
30
+	Password      string `json:"password,omitempty"`
31
+	Auth          string `json:"auth"`
32
+	Email         string `json:"email"`
33
+	ServerAddress string `json:"serveraddress,omitempty"`
33 34
 }
34 35
 
35 36
 type ConfigFile struct {
... ...
@@ -96,6 +97,7 @@ func LoadConfig(rootPath string) (*ConfigFile, error) {
96 96
 		}
97 97
 		origEmail := strings.Split(arr[1], " = ")
98 98
 		authConfig.Email = origEmail[1]
99
+		authConfig.ServerAddress = IndexServerAddress()
99 100
 		configFile.Configs[IndexServerAddress()] = authConfig
100 101
 	} else {
101 102
 		for k, authConfig := range configFile.Configs {
... ...
@@ -105,6 +107,7 @@ func LoadConfig(rootPath string) (*ConfigFile, error) {
105 105
 			}
106 106
 			authConfig.Auth = ""
107 107
 			configFile.Configs[k] = authConfig
108
+			authConfig.ServerAddress = k
108 109
 		}
109 110
 	}
110 111
 	return &configFile, nil
... ...
@@ -125,7 +128,7 @@ func SaveConfig(configFile *ConfigFile) error {
125 125
 		authCopy.Auth = encodeAuth(&authCopy)
126 126
 		authCopy.Username = ""
127 127
 		authCopy.Password = ""
128
-
128
+		authCopy.ServerAddress = ""
129 129
 		configs[k] = authCopy
130 130
 	}
131 131
 
... ...
@@ -146,14 +149,26 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e
146 146
 	reqStatusCode := 0
147 147
 	var status string
148 148
 	var reqBody []byte
149
-	jsonBody, err := json.Marshal(authConfig)
149
+
150
+	serverAddress := authConfig.ServerAddress
151
+	if serverAddress == "" {
152
+		serverAddress = IndexServerAddress()
153
+	}
154
+
155
+	loginAgainstOfficialIndex := serverAddress == IndexServerAddress()
156
+
157
+	// to avoid sending the server address to the server it should be removed before marshalled
158
+	authCopy := *authConfig
159
+	authCopy.ServerAddress = ""
160
+
161
+	jsonBody, err := json.Marshal(authCopy)
150 162
 	if err != nil {
151 163
 		return "", fmt.Errorf("Config Error: %s", err)
152 164
 	}
153 165
 
154 166
 	// using `bytes.NewReader(jsonBody)` here causes the server to respond with a 411 status.
155 167
 	b := strings.NewReader(string(jsonBody))
156
-	req1, err := http.Post(IndexServerAddress()+"users/", "application/json; charset=utf-8", b)
168
+	req1, err := http.Post(serverAddress+"users/", "application/json; charset=utf-8", b)
157 169
 	if err != nil {
158 170
 		return "", fmt.Errorf("Server Error: %s", err)
159 171
 	}
... ...
@@ -165,14 +180,23 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e
165 165
 	}
166 166
 
167 167
 	if reqStatusCode == 201 {
168
-		status = "Account created. Please use the confirmation link we sent" +
169
-			" to your e-mail to activate it."
168
+		if loginAgainstOfficialIndex {
169
+			status = "Account created. Please use the confirmation link we sent" +
170
+				" to your e-mail to activate it."
171
+		} else {
172
+			status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it."
173
+		}
170 174
 	} else if reqStatusCode == 403 {
171
-		return "", fmt.Errorf("Login: Your account hasn't been activated. " +
172
-			"Please check your e-mail for a confirmation link.")
175
+		if loginAgainstOfficialIndex {
176
+			return "", fmt.Errorf("Login: Your account hasn't been activated. " +
177
+				"Please check your e-mail for a confirmation link.")
178
+		} else {
179
+			return "", fmt.Errorf("Login: Your account hasn't been activated. " +
180
+				"Please see the documentation of the registry " + serverAddress + " for instructions how to activate it.")
181
+		}
173 182
 	} else if reqStatusCode == 400 {
174 183
 		if string(reqBody) == "\"Username or email already exists\"" {
175
-			req, err := factory.NewRequest("GET", IndexServerAddress()+"users/", nil)
184
+			req, err := factory.NewRequest("GET", serverAddress+"users/", nil)
176 185
 			req.SetBasicAuth(authConfig.Username, authConfig.Password)
177 186
 			resp, err := client.Do(req)
178 187
 			if err != nil {
... ...
@@ -199,3 +223,52 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e
199 199
 	}
200 200
 	return status, nil
201 201
 }
202
+
203
+// this method matches a auth configuration to a server address or a url
204
+func (config *ConfigFile) ResolveAuthConfig(registry string) AuthConfig {
205
+	if registry == IndexServerAddress() || len(registry) == 0 {
206
+		// default to the index server
207
+		return config.Configs[IndexServerAddress()]
208
+	}
209
+	// if its not the index server there are three cases:
210
+	//
211
+	// 1. this is a full config url -> it should be used as is
212
+	// 2. it could be a full url, but with the wrong protocol
213
+	// 3. it can be the hostname optionally with a port
214
+	//
215
+	// as there is only one auth entry which is fully qualified we need to start
216
+	// parsing and matching
217
+
218
+	swapProtocoll := func(url string) string {
219
+		if strings.HasPrefix(url, "http:") {
220
+			return strings.Replace(url, "http:", "https:", 1)
221
+		}
222
+		if strings.HasPrefix(url, "https:") {
223
+			return strings.Replace(url, "https:", "http:", 1)
224
+		}
225
+		return url
226
+	}
227
+
228
+	resolveIgnoringProtocol := func(url string) AuthConfig {
229
+		if c, found := config.Configs[url]; found {
230
+			return c
231
+		}
232
+		registrySwappedProtocoll := swapProtocoll(url)
233
+		// now try to match with the different protocol
234
+		if c, found := config.Configs[registrySwappedProtocoll]; found {
235
+			return c
236
+		}
237
+		return AuthConfig{}
238
+	}
239
+
240
+	// match both protocols as it could also be a server name like httpfoo
241
+	if strings.HasPrefix(registry, "http:") || strings.HasPrefix(registry, "https:") {
242
+		return resolveIgnoringProtocol(registry)
243
+	}
244
+
245
+	url := "https://" + registry
246
+	if !strings.Contains(registry, "/") {
247
+		url = url + "/v1/"
248
+	}
249
+	return resolveIgnoringProtocol(url)
250
+}
... ...
@@ -4,10 +4,12 @@ import (
4 4
 	"archive/tar"
5 5
 	"bufio"
6 6
 	"bytes"
7
+	"encoding/base64"
7 8
 	"encoding/json"
8 9
 	"flag"
9 10
 	"fmt"
10 11
 	"github.com/dotcloud/docker/auth"
12
+	"github.com/dotcloud/docker/registry"
11 13
 	"github.com/dotcloud/docker/term"
12 14
 	"github.com/dotcloud/docker/utils"
13 15
 	"io"
... ...
@@ -91,6 +93,7 @@ func (cli *DockerCli) CmdHelp(args ...string) error {
91 91
 		{"login", "Register or Login to the docker registry server"},
92 92
 		{"logs", "Fetch the logs of a container"},
93 93
 		{"port", "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT"},
94
+		{"top", "Lookup the running processes of a container"},
94 95
 		{"ps", "List containers"},
95 96
 		{"pull", "Pull an image or a repository from the docker registry server"},
96 97
 		{"push", "Push an image or a repository to the docker registry server"},
... ...
@@ -102,7 +105,6 @@ func (cli *DockerCli) CmdHelp(args ...string) error {
102 102
 		{"start", "Start a stopped container"},
103 103
 		{"stop", "Stop a running container"},
104 104
 		{"tag", "Tag an image into a repository"},
105
-		{"top", "Lookup the running processes of a container"},
106 105
 		{"version", "Show the docker version information"},
107 106
 		{"wait", "Block until a container stops, then print its exit code"},
108 107
 	} {
... ...
@@ -187,10 +189,8 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
187 187
 	} else if utils.IsURL(cmd.Arg(0)) || utils.IsGIT(cmd.Arg(0)) {
188 188
 		isRemote = true
189 189
 	} else {
190
-		if fi, err := os.Stat(cmd.Arg(0)); err != nil {
190
+		if _, err := os.Stat(cmd.Arg(0)); err != nil {
191 191
 			return err
192
-		} else if !fi.IsDir() {
193
-			return fmt.Errorf("\"%s\" is not a path or URL. Please provide a path to a directory containing a Dockerfile.", cmd.Arg(0))
194 192
 		}
195 193
 		context, err = Tar(cmd.Arg(0), Uncompressed)
196 194
 	}
... ...
@@ -254,7 +254,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
254 254
 
255 255
 // 'docker login': login / register a user to registry service.
256 256
 func (cli *DockerCli) CmdLogin(args ...string) error {
257
-	cmd := Subcmd("login", "[OPTIONS]", "Register or Login to the docker registry server")
257
+	cmd := Subcmd("login", "[OPTIONS] [SERVER]", "Register or Login to a docker registry server, if no server is specified \""+auth.IndexServerAddress()+"\" is the default.")
258 258
 
259 259
 	var username, password, email string
260 260
 
... ...
@@ -262,10 +262,17 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
262 262
 	cmd.StringVar(&password, "p", "", "password")
263 263
 	cmd.StringVar(&email, "e", "", "email")
264 264
 	err := cmd.Parse(args)
265
-
266 265
 	if err != nil {
267 266
 		return nil
268 267
 	}
268
+	serverAddress := auth.IndexServerAddress()
269
+	if len(cmd.Args()) > 0 {
270
+		serverAddress, err = registry.ExpandAndVerifyRegistryUrl(cmd.Arg(0))
271
+		if err != nil {
272
+			return err
273
+		}
274
+		fmt.Fprintf(cli.out, "Login against server at %s\n", serverAddress)
275
+	}
269 276
 
270 277
 	promptDefault := func(prompt string, configDefault string) {
271 278
 		if configDefault == "" {
... ...
@@ -298,19 +305,16 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
298 298
 			username = authconfig.Username
299 299
 		}
300 300
 	}
301
-
302 301
 	if username != authconfig.Username {
303 302
 		if password == "" {
304 303
 			oldState, _ := term.SaveState(cli.terminalFd)
305 304
 			fmt.Fprintf(cli.out, "Password: ")
306
-
307 305
 			term.DisableEcho(cli.terminalFd, oldState)
308 306
 
309 307
 			password = readInput(cli.in, cli.out)
310 308
 			fmt.Fprint(cli.out, "\n")
311 309
 
312 310
 			term.RestoreTerminal(cli.terminalFd, oldState)
313
-
314 311
 			if password == "" {
315 312
 				return fmt.Errorf("Error : Password Required")
316 313
 			}
... ...
@@ -327,15 +331,15 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
327 327
 		password = authconfig.Password
328 328
 		email = authconfig.Email
329 329
 	}
330
-
331 330
 	authconfig.Username = username
332 331
 	authconfig.Password = password
333 332
 	authconfig.Email = email
334
-	cli.configFile.Configs[auth.IndexServerAddress()] = authconfig
333
+	authconfig.ServerAddress = serverAddress
334
+	cli.configFile.Configs[serverAddress] = authconfig
335 335
 
336
-	body, statusCode, err := cli.call("POST", "/auth", cli.configFile.Configs[auth.IndexServerAddress()])
336
+	body, statusCode, err := cli.call("POST", "/auth", cli.configFile.Configs[serverAddress])
337 337
 	if statusCode == 401 {
338
-		delete(cli.configFile.Configs, auth.IndexServerAddress())
338
+		delete(cli.configFile.Configs, serverAddress)
339 339
 		auth.SaveConfig(cli.configFile)
340 340
 		return err
341 341
 	}
... ...
@@ -812,6 +816,13 @@ func (cli *DockerCli) CmdPush(args ...string) error {
812 812
 
813 813
 	cli.LoadConfigFile()
814 814
 
815
+	// Resolve the Repository name from fqn to endpoint + name
816
+	endpoint, _, err := registry.ResolveRepositoryName(name)
817
+	if err != nil {
818
+		return err
819
+	}
820
+	// Resolve the Auth config relevant for this server
821
+	authConfig := cli.configFile.ResolveAuthConfig(endpoint)
815 822
 	// If we're not using a custom registry, we know the restrictions
816 823
 	// applied to repository names and can warn the user in advance.
817 824
 	// Custom repositories can have different rules, and we must also
... ...
@@ -825,8 +836,8 @@ func (cli *DockerCli) CmdPush(args ...string) error {
825 825
 	}
826 826
 
827 827
 	v := url.Values{}
828
-	push := func() error {
829
-		buf, err := json.Marshal(cli.configFile.Configs[auth.IndexServerAddress()])
828
+	push := func(authConfig auth.AuthConfig) error {
829
+		buf, err := json.Marshal(authConfig)
830 830
 		if err != nil {
831 831
 			return err
832 832
 		}
... ...
@@ -834,13 +845,14 @@ func (cli *DockerCli) CmdPush(args ...string) error {
834 834
 		return cli.stream("POST", "/images/"+name+"/push?"+v.Encode(), bytes.NewBuffer(buf), cli.out)
835 835
 	}
836 836
 
837
-	if err := push(); err != nil {
838
-		if err.Error() == "Authentication is required." {
837
+	if err := push(authConfig); err != nil {
838
+		if err.Error() == registry.ErrLoginRequired.Error() {
839 839
 			fmt.Fprintln(cli.out, "\nPlease login prior to push:")
840
-			if err := cli.CmdLogin(""); err != nil {
840
+			if err := cli.CmdLogin(endpoint); err != nil {
841 841
 				return err
842 842
 			}
843
-			return push()
843
+			authConfig := cli.configFile.ResolveAuthConfig(endpoint)
844
+			return push(authConfig)
844 845
 		}
845 846
 		return err
846 847
 	}
... ...
@@ -864,11 +876,39 @@ func (cli *DockerCli) CmdPull(args ...string) error {
864 864
 		*tag = parsedTag
865 865
 	}
866 866
 
867
+	// Resolve the Repository name from fqn to endpoint + name
868
+	endpoint, _, err := registry.ResolveRepositoryName(remote)
869
+	if err != nil {
870
+		return err
871
+	}
872
+
873
+	cli.LoadConfigFile()
874
+
875
+	// Resolve the Auth config relevant for this server
876
+	authConfig := cli.configFile.ResolveAuthConfig(endpoint)
867 877
 	v := url.Values{}
868 878
 	v.Set("fromImage", remote)
869 879
 	v.Set("tag", *tag)
870 880
 
871
-	if err := cli.stream("POST", "/images/create?"+v.Encode(), nil, cli.out); err != nil {
881
+	pull := func(authConfig auth.AuthConfig) error {
882
+		buf, err := json.Marshal(authConfig)
883
+		if err != nil {
884
+			return err
885
+		}
886
+		v.Set("authConfig", base64.URLEncoding.EncodeToString(buf))
887
+
888
+		return cli.stream("POST", "/images/create?"+v.Encode(), bytes.NewBuffer(buf), cli.out)
889
+	}
890
+
891
+	if err := pull(authConfig); err != nil {
892
+		if err.Error() == registry.ErrLoginRequired.Error() {
893
+			fmt.Fprintln(cli.out, "\nPlease login prior to push:")
894
+			if err := cli.CmdLogin(endpoint); err != nil {
895
+				return err
896
+			}
897
+			authConfig := cli.configFile.ResolveAuthConfig(endpoint)
898
+			return pull(authConfig)
899
+		}
872 900
 		return err
873 901
 	}
874 902
 
... ...
@@ -991,7 +991,8 @@ Check auth configuration
991 991
 	   {
992 992
 		"username":"hannibal",
993 993
 		"password:"xxxx",
994
-		"email":"hannibal@a-team.com"
994
+		"email":"hannibal@a-team.com",
995
+		"serveraddress":"https://index.docker.io/v1/"
995 996
 	   }
996 997
 
997 998
         **Example response**:
... ...
@@ -8,10 +8,17 @@
8 8
 
9 9
 ::
10 10
 
11
-    Usage: docker login [OPTIONS]
11
+    Usage: docker login [OPTIONS] [SERVER]
12 12
 
13 13
     Register or Login to the docker registry server
14 14
 
15 15
     -e="": email
16 16
     -p="": password
17 17
     -u="": username
18
+
19
+    If you want to login to a private registry you can
20
+    specify this by adding the server name.
21
+
22
+    example:
23
+    docker login localhost:8080
24
+
... ...
@@ -22,6 +22,7 @@ import (
22 22
 var (
23 23
 	ErrAlreadyExists         = errors.New("Image already exists")
24 24
 	ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")")
25
+	ErrLoginRequired         = errors.New("Authentication is required.")
25 26
 )
26 27
 
27 28
 func pingRegistryEndpoint(endpoint string) error {
... ...
@@ -102,17 +103,38 @@ func ResolveRepositoryName(reposName string) (string, string, error) {
102 102
 	if err := validateRepositoryName(reposName); err != nil {
103 103
 		return "", "", err
104 104
 	}
105
+	endpoint, err := ExpandAndVerifyRegistryUrl(hostname)
106
+	if err != nil {
107
+		return "", "", err
108
+	}
109
+	return endpoint, reposName, err
110
+}
111
+
112
+// this method expands the registry name as used in the prefix of a repo
113
+// to a full url. if it already is a url, there will be no change.
114
+// The registry is pinged to test if it http or https
115
+func ExpandAndVerifyRegistryUrl(hostname string) (string, error) {
116
+	if strings.HasPrefix(hostname, "http:") || strings.HasPrefix(hostname, "https:") {
117
+		// if there is no slash after https:// (8 characters) then we have no path in the url
118
+		if strings.LastIndex(hostname, "/") < 9 {
119
+			// there is no path given. Expand with default path
120
+			hostname = hostname + "/v1/"
121
+		}
122
+		if err := pingRegistryEndpoint(hostname); err != nil {
123
+			return "", errors.New("Invalid Registry endpoint: " + err.Error())
124
+		}
125
+		return hostname, nil
126
+	}
105 127
 	endpoint := fmt.Sprintf("https://%s/v1/", hostname)
106 128
 	if err := pingRegistryEndpoint(endpoint); err != nil {
107 129
 		utils.Debugf("Registry %s does not work (%s), falling back to http", endpoint, err)
108 130
 		endpoint = fmt.Sprintf("http://%s/v1/", hostname)
109 131
 		if err = pingRegistryEndpoint(endpoint); err != nil {
110 132
 			//TODO: triggering highland build can be done there without "failing"
111
-			return "", "", errors.New("Invalid Registry endpoint: " + err.Error())
133
+			return "", errors.New("Invalid Registry endpoint: " + err.Error())
112 134
 		}
113 135
 	}
114
-	err := validateRepositoryName(reposName)
115
-	return endpoint, reposName, err
136
+	return endpoint, nil
116 137
 }
117 138
 
118 139
 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
139 139
 	req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
140 140
 	res, err := doWithCookies(r.client, req)
141 141
 	if err != nil || res.StatusCode != 200 {
142
+		if res.StatusCode == 401 {
143
+			return nil, ErrLoginRequired
144
+		}
142 145
 		if res != nil {
143 146
 			return nil, utils.NewHTTPRequestError(fmt.Sprintf("Internal server error: %d trying to fetch remote history for %s", res.StatusCode, imgID), res)
144 147
 		}
... ...
@@ -282,7 +307,7 @@ func (r *Registry) GetRepositoryData(indexEp, remote string) (*RepositoryData, e
282 282
 	}
283 283
 	defer res.Body.Close()
284 284
 	if res.StatusCode == 401 {
285
-		return nil, utils.NewHTTPRequestError(fmt.Sprintf("Please login first (HTTP code %d)", res.StatusCode), res)
285
+		return nil, ErrLoginRequired
286 286
 	}
287 287
 	// TODO: Right now we're ignoring checksums in the response body.
288 288
 	// In the future, we need to use them to check image validity.
... ...
@@ -655,6 +655,9 @@ func (srv *Server) ImagePull(localName string, tag string, out io.Writer, sf *ut
655 655
 
656 656
 	out = utils.NewWriteFlusher(out)
657 657
 	err = srv.pullRepository(r, out, localName, remoteName, tag, endpoint, sf, parallel)
658
+	if err == registry.ErrLoginRequired {
659
+		return err
660
+	}
658 661
 	if err != nil {
659 662
 		if err := srv.pullImage(r, out, remoteName, endpoint, nil, sf); err != nil {
660 663
 			return err