Merge lp:~axwalk/juju-core/null-provider-storage-auth into lp:~go-bot/juju-core/trunk

Proposed by Andrew Wilkins
Status: Merged
Approved by: Andrew Wilkins
Approved revision: no longer in the source branch.
Merged at revision: 1921
Proposed branch: lp:~axwalk/juju-core/null-provider-storage-auth
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 1012 lines (+525/-98)
12 files modified
environs/httpstorage/backend.go (+56/-19)
environs/httpstorage/backend_test.go (+27/-7)
environs/httpstorage/storage.go (+55/-26)
environs/httpstorage/storage_test.go (+35/-14)
environs/manual/bootstrap.go (+3/-6)
provider/null/config.go (+6/-0)
provider/null/config_test.go (+4/-3)
provider/null/environ.go (+51/-1)
provider/null/provider.go (+9/-2)
worker/localstorage/config.go (+112/-0)
worker/localstorage/config_test.go (+132/-0)
worker/localstorage/worker.go (+35/-20)
To merge this branch: bzr merge lp:~axwalk/juju-core/null-provider-storage-auth
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+187964@code.launchpad.net

Commit message

Wire up authenticating httpstorage

There are two things happening here:
 - httpstorage authentication changes
 - wiring up to null provider/localstorage worker.

The httpstorage authentication mechanism has
changed to use an auth key (randomly generated
as part of the boilerplate), rather than client
certificates. This gives us more options for
using the storage, since now you don't need the
client to share the CA key.

Also, when serving HTTPS, HTTP is *also* provided,
but only for non-modifying operations. This allows
wget et al. to fetch files from the storage without
bypassing certificate validation. The HTTP interface
disallows modifying operations if there is an HTTPS
interface backing it. To get an HTTPS URL, a HEAD
request may be performed on the HTTP interface.

https://codereview.appspot.com/14011044/

Description of the change

Wire up authenticating httpstorage

There are two things happening here:
 - httpstorage authentication changes
 - wiring up to null provider/localstorage worker.

The httpstorage authentication mechanism has
changed to use an auth key (randomly generated
as part of the boilerplate), rather than client
certificates. This gives us more options for
using the storage, since now you don't need the
client to share the CA key.

Also, when serving HTTPS, HTTP is *also* provided,
but only for non-modifying operations. This allows
wget et al. to fetch files from the storage without
bypassing certificate validation. The HTTP interface
disallows modifying operations if there is an HTTPS
interface backing it. To get an HTTPS URL, a HEAD
request may be performed on the HTTP interface.

https://codereview.appspot.com/14011044/

To post a comment you must log in.
Revision history for this message
Andrew Wilkins (axwalk) wrote :

Reviewers: mp+187964_code.launchpad.net,

Message:
Please take a look.

Description:
Wire up authenticating httpstorage

There are two things happening here:
  - httpstorage authentication changes
  - wiring up to null provider/localstorage worker.

The httpstorage authentication mechanism has
changed to use an auth key (randomly generated
as part of the boilerplate), rather than client
certificates. This gives us more options for
using the storage, since now you don't need the
client to share the CA key.

Also, when serving HTTPS, HTTP is *also* provided,
but only for non-modifying operations. This allows
wget et al. to fetch files from the storage without
bypassing certificate validation. The HTTP interface
disallows modifying operations if there is an HTTPS
interface backing it. To get an HTTPS URL, a HEAD
request may be performed on the HTTP interface.

https://code.launchpad.net/~axwalk/juju-core/null-provider-storage-auth/+merge/187964

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/14011044/

Affected files (+507, -108 lines):
   A [revision details]
   M environs/httpstorage/backend.go
   M environs/httpstorage/backend_test.go
   M environs/httpstorage/storage.go
   M environs/httpstorage/storage_test.go
   M environs/manual/bootstrap.go
   M provider/null/config.go
   M provider/null/config_test.go
   M provider/null/environ.go
   M provider/null/environ_test.go
   M provider/null/provider.go
   M worker/localstorage/config.go
   A worker/localstorage/config_test.go
   M worker/localstorage/worker.go

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
Tim Penhey (thumper) wrote :

LGTM, few comments

https://codereview.appspot.com/14011044/diff/4001/environs/httpstorage/backend.go
File environs/httpstorage/backend.go (right):

https://codereview.appspot.com/14011044/diff/4001/environs/httpstorage/backend.go#newcode78
environs/httpstorage/backend.go:78: http.Error(w, "method HEAD is not
supported", http.StatusMethodNotAllowed)
shouldn't we return here? So as not to change the header to 200 OK?

https://codereview.appspot.com/14011044/diff/4001/provider/null/environ.go
File provider/null/environ.go (right):

https://codereview.appspot.com/14011044/diff/4001/provider/null/environ.go#newcode150
provider/null/environ.go:150: return
httpstorage.Client(e.envConfig().storageAddr())
If this is an error condition, shouldn't we log an error (which the
users now see) and return nil rather than http storage?

https://codereview.appspot.com/14011044/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

https://codereview.appspot.com/14011044/diff/4001/environs/httpstorage/backend.go
File environs/httpstorage/backend.go (right):

https://codereview.appspot.com/14011044/diff/4001/environs/httpstorage/backend.go#newcode78
environs/httpstorage/backend.go:78: http.Error(w, "method HEAD is not
supported", http.StatusMethodNotAllowed)
On 2013/10/02 05:07:25, thumper wrote:
> shouldn't we return here? So as not to change the header to 200 OK?

It doesn't actually change in practice, as the first header wins. But
this was an oversight and I've fixed it. I added a test while I was at
it. Thanks.

https://codereview.appspot.com/14011044/diff/4001/provider/null/environ.go
File provider/null/environ.go (right):

https://codereview.appspot.com/14011044/diff/4001/provider/null/environ.go#newcode150
provider/null/environ.go:150: return
httpstorage.Client(e.envConfig().storageAddr())
On 2013/10/02 05:07:25, thumper wrote:
> If this is an error condition, shouldn't we log an error (which the
users now
> see) and return nil rather than http storage?

Done.

https://codereview.appspot.com/14011044/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'environs/httpstorage/backend.go'
2--- environs/httpstorage/backend.go 2013-09-24 05:42:43 +0000
3+++ environs/httpstorage/backend.go 2013-10-02 05:48:33 +0000
4@@ -18,24 +18,39 @@
5 "launchpad.net/juju-core/environs/storage"
6 )
7
8-// TODO(axw) 2013-09-16 bug #1225916
9-// Implement authentication for modifying storage.
10-
11 // storageBackend provides HTTP access to a storage object.
12 type storageBackend struct {
13 backend storage.Storage
14- tls bool
15+
16+ // httpsBaseURL is the base URL to send to clients
17+ // if they perform a HEAD request.
18+ httpsBaseURL string
19+
20+ // authkey is non-empty if modifying requests
21+ // require an auth key.
22+ authkey string
23 }
24
25 // ServeHTTP handles the HTTP requests to the container.
26 func (s *storageBackend) ServeHTTP(w http.ResponseWriter, req *http.Request) {
27 switch req.Method {
28+ case "PUT", "DELETE":
29+ // Don't allow modifying operations if there's an HTTPS backend
30+ // to handle that, and ensure the user is authorised/authenticated.
31+ if s.httpsBaseURL != "" || !s.authorised(req) {
32+ http.Error(w, "unauthorised access", http.StatusUnauthorized)
33+ return
34+ }
35+ }
36+ switch req.Method {
37 case "GET":
38 if strings.HasSuffix(req.URL.Path, "*") {
39 s.handleList(w, req)
40 } else {
41 s.handleGet(w, req)
42 }
43+ case "HEAD":
44+ s.handleHead(w, req)
45 case "PUT":
46 s.handlePut(w, req)
47 case "DELETE":
48@@ -45,11 +60,25 @@
49 }
50 }
51
52-// authorised checks that either the storage does not require authorisation,
53-// or the user has negotiated TLS with a valid certificate. Authentication
54-// implies authorisation.
55+// authorised checks that either the storage does not require
56+// authorisation, or the user has specified the correct auth key.
57 func (s *storageBackend) authorised(req *http.Request) bool {
58- return !s.tls || len(req.TLS.PeerCertificates) > 0
59+ if s.authkey == "" {
60+ return true
61+ }
62+ return req.URL.Query().Get("authkey") == s.authkey
63+}
64+
65+// handleHead returns the HTTPS URL for the specified
66+// path in the Location header.
67+func (s *storageBackend) handleHead(w http.ResponseWriter, req *http.Request) {
68+ if s.httpsBaseURL != "" {
69+ w.Header().Set("Location", s.httpsBaseURL+req.URL.Path)
70+ } else {
71+ http.Error(w, "method HEAD is not supported", http.StatusMethodNotAllowed)
72+ return
73+ }
74+ w.WriteHeader(http.StatusOK)
75 }
76
77 // handleGet returns a storage file to the client.
78@@ -85,10 +114,6 @@
79
80 // handlePut stores data from the client in the storage.
81 func (s *storageBackend) handlePut(w http.ResponseWriter, req *http.Request) {
82- if !s.authorised(req) {
83- http.Error(w, "unauthorised access", http.StatusUnauthorized)
84- return
85- }
86 if req.ContentLength < 0 {
87 http.Error(w, "missing or invalid Content-Length header", http.StatusInternalServerError)
88 return
89@@ -119,7 +144,7 @@
90 // requests to the given storage implementation. It returns the network
91 // listener. This can then be attached to with Client.
92 func Serve(addr string, stor storage.Storage) (net.Listener, error) {
93- return serve(addr, stor, nil)
94+ return serve(addr, stor, nil, "")
95 }
96
97 // ServeTLS runs a storage server on the given network address, relaying
98@@ -130,7 +155,7 @@
99 //
100 // This method returns the network listener, which can then be attached
101 // to with ClientTLS.
102-func ServeTLS(addr string, stor storage.Storage, caCertPEM, caKeyPEM []byte, hostnames []string) (net.Listener, error) {
103+func ServeTLS(addr string, stor storage.Storage, caCertPEM, caKeyPEM []byte, hostnames []string, authkey string) (net.Listener, error) {
104 expiry := time.Now().UTC().AddDate(10, 0, 0)
105 certPEM, keyPEM, err := cert.NewServer(caCertPEM, caKeyPEM, expiry, hostnames)
106 if err != nil {
107@@ -150,21 +175,33 @@
108 ClientAuth: tls.VerifyClientCertIfGiven,
109 ClientCAs: caCerts,
110 }
111- return serve(addr, stor, config)
112+ return serve(addr, stor, config, authkey)
113 }
114
115-func serve(addr string, stor storage.Storage, tlsConfig *tls.Config) (net.Listener, error) {
116+func serve(addr string, stor storage.Storage, tlsConfig *tls.Config, authkey string) (net.Listener, error) {
117 listener, err := net.Listen("tcp", addr)
118 if err != nil {
119 return nil, fmt.Errorf("cannot start listener: %v", err)
120 }
121 backend := &storageBackend{backend: stor}
122 if tlsConfig != nil {
123- listener = tls.NewListener(listener, tlsConfig)
124- backend.tls = true
125+ tlsBackend := &storageBackend{backend: stor, authkey: authkey}
126+ tcpAddr := listener.Addr().(*net.TCPAddr)
127+ tlsListener, err := tls.Listen("tcp", fmt.Sprintf("[%s]:0", tcpAddr.IP), tlsConfig)
128+ if err != nil {
129+ listener.Close()
130+ return nil, fmt.Errorf("cannot start TLS listener: %v", err)
131+ }
132+ backend.httpsBaseURL = fmt.Sprintf("https://%s", tlsListener.Addr())
133+ goServe(tlsListener, tlsBackend)
134 }
135+ goServe(listener, backend)
136+ return listener, nil
137+}
138+
139+func goServe(listener net.Listener, backend *storageBackend) {
140+ // Construct a NewServeMux to sanitise request paths.
141 mux := http.NewServeMux()
142 mux.Handle("/", backend)
143 go http.Serve(listener, mux)
144- return listener, nil
145 }
146
147=== modified file 'environs/httpstorage/backend_test.go'
148--- environs/httpstorage/backend_test.go 2013-09-24 05:42:43 +0000
149+++ environs/httpstorage/backend_test.go 2013-10-02 05:48:33 +0000
150@@ -25,6 +25,8 @@
151 "launchpad.net/juju-core/testing/testbase"
152 )
153
154+const testAuthkey = "jabberwocky"
155+
156 func TestLocal(t *stdtesting.T) {
157 gc.TestingT(t)
158 }
159@@ -50,14 +52,16 @@
160 // startServerTLS starts a new TLS-based local storage server
161 // using a temporary directory and returns the listener,
162 // a base URL for the server and the directory path.
163-func startServerTLS(c *gc.C, caCertPEM, caKeyPEM []byte) (listener net.Listener, url, dataDir string) {
164+func startServerTLS(c *gc.C) (listener net.Listener, url, dataDir string) {
165 dataDir = c.MkDir()
166 embedded, err := filestorage.NewFileStorageWriter(dataDir, filestorage.UseDefaultTmpDir)
167 c.Assert(err, gc.IsNil)
168 hostnames := []string{"127.0.0.1"}
169- listener, err = httpstorage.ServeTLS("127.0.0.1:0", embedded, caCertPEM, caKeyPEM, hostnames)
170+ caCertPEM := []byte(coretesting.CACert)
171+ caKeyPEM := []byte(coretesting.CAKey)
172+ listener, err = httpstorage.ServeTLS("127.0.0.1:0", embedded, caCertPEM, caKeyPEM, hostnames, testAuthkey)
173 c.Assert(err, gc.IsNil)
174- return listener, fmt.Sprintf("https://%s/", listener.Addr()), dataDir
175+ return listener, fmt.Sprintf("http://%s/", listener.Addr()), dataDir
176 }
177
178 type testCase struct {
179@@ -130,6 +134,24 @@
180 },
181 }
182
183+func (s *backendSuite) TestHead(c *gc.C) {
184+ // HEAD is unsupported for non-authenticating servers.
185+ listener, url, _ := startServer(c)
186+ defer listener.Close()
187+ resp, err := http.Head(url)
188+ c.Assert(err, gc.IsNil)
189+ c.Assert(resp.StatusCode, gc.Equals, http.StatusMethodNotAllowed)
190+
191+ // HEAD on an authenticating server will return the HTTPS counterpart URL.
192+ client, url, _ := s.tlsServerAndClient(c)
193+ resp, err = client.Head(url + "arbitrary")
194+ c.Assert(err, gc.IsNil)
195+ c.Assert(resp.StatusCode, gc.Equals, http.StatusOK)
196+ location, err := resp.Location()
197+ c.Assert(err, gc.IsNil)
198+ c.Assert(location.String(), gc.Matches, "https://127.0.0.1:[0-9]{5}/arbitrary")
199+}
200+
201 func (s *backendSuite) TestGet(c *gc.C) {
202 // Test retrieving a file from a storage.
203 listener, url, dataDir := startServer(c)
204@@ -373,12 +395,10 @@
205 }
206
207 func (b *backendSuite) tlsServerAndClient(c *gc.C) (client *http.Client, url, dataDir string) {
208- caCertPEM := []byte(coretesting.CACert)
209- caKeyPEM := []byte(coretesting.CAKey)
210- listener, url, dataDir := startServerTLS(c, caCertPEM, caKeyPEM)
211+ listener, url, dataDir := startServerTLS(c)
212 b.AddCleanup(func(*gc.C) { listener.Close() })
213 caCerts := x509.NewCertPool()
214- c.Assert(caCerts.AppendCertsFromPEM(caCertPEM), jc.IsTrue)
215+ c.Assert(caCerts.AppendCertsFromPEM([]byte(coretesting.CACert)), jc.IsTrue)
216 client = &http.Client{
217 Transport: &http.Transport{
218 TLSClientConfig: &tls.Config{RootCAs: caCerts},
219
220=== modified file 'environs/httpstorage/storage.go'
221--- environs/httpstorage/storage.go 2013-09-27 05:19:30 +0000
222+++ environs/httpstorage/storage.go 2013-10-02 05:48:33 +0000
223@@ -11,11 +11,11 @@
224 "io"
225 "io/ioutil"
226 "net/http"
227+ "net/url"
228 "sort"
229 "strings"
230- "time"
231+ "sync"
232
233- "launchpad.net/juju-core/cert"
234 "launchpad.net/juju-core/environs/storage"
235 coreerrors "launchpad.net/juju-core/errors"
236 "launchpad.net/juju-core/utils"
237@@ -23,47 +23,60 @@
238
239 // storage implements the storage.Storage interface.
240 type localStorage struct {
241- baseURL string
242- client *http.Client
243+ addr string
244+ client *http.Client
245+
246+ authkey string
247+ httpsBaseURL string
248+ httpsBaseURLError error
249+ httpsBaseURLOnce sync.Once
250 }
251
252 // Client returns a storage object that will talk to the
253 // storage server at the given network address (see Serve)
254 func Client(addr string) storage.Storage {
255 return &localStorage{
256- baseURL: fmt.Sprintf("http://%s/", addr),
257- client: http.DefaultClient,
258+ addr: addr,
259+ client: http.DefaultClient,
260 }
261 }
262
263 // ClientTLS returns a storage object that will talk to the
264 // storage server at the given network address (see Serve),
265-// using TLS. The client will generate a client certificate,
266-// signed with the given CA certificate/key, to authenticate.
267-func ClientTLS(addr string, caCertPEM, caKeyPEM []byte) (storage.Storage, error) {
268+// using TLS. The client is given an authentication key,
269+// which the server will verify for Put and Remove* operations.
270+func ClientTLS(addr string, caCertPEM []byte, authkey string) (storage.Storage, error) {
271 caCerts := x509.NewCertPool()
272- if !caCerts.AppendCertsFromPEM(caCertPEM) {
273- return nil, errors.New("error adding CA certificate to pool")
274- }
275- expiry := time.Now().UTC().AddDate(10, 0, 0)
276- clientCertPEM, clientKeyPEM, err := cert.NewClient(caCertPEM, caKeyPEM, expiry)
277- clientCert, err := tls.X509KeyPair(clientCertPEM, clientKeyPEM)
278- if err != nil {
279- return nil, err
280+ if caCertPEM != nil {
281+ if !caCerts.AppendCertsFromPEM(caCertPEM) {
282+ return nil, errors.New("error adding CA certificate to pool")
283+ }
284 }
285 return &localStorage{
286- baseURL: fmt.Sprintf("https://%s/", addr),
287+ addr: addr,
288+ authkey: authkey,
289 client: &http.Client{
290 Transport: &http.Transport{
291- TLSClientConfig: &tls.Config{
292- Certificates: []tls.Certificate{clientCert},
293- RootCAs: caCerts,
294- },
295+ TLSClientConfig: &tls.Config{RootCAs: caCerts},
296 },
297 },
298 }, nil
299 }
300
301+func (s *localStorage) getHTTPSBaseURL() (string, error) {
302+ url, _ := s.URL("/") // never fails
303+ resp, err := s.client.Head(url)
304+ if err != nil {
305+ return "", err
306+ }
307+ resp.Body.Close()
308+ httpsURL, err := resp.Location()
309+ if err != nil {
310+ return "", err
311+ }
312+ return httpsURL.String(), nil
313+}
314+
315 // Get opens the given storage file and returns a ReadCloser
316 // that can be used to read its contents. It is the caller's
317 // responsibility to close it after use. If the name does not
318@@ -119,9 +132,25 @@
319 return names, nil
320 }
321
322-// URL returns an URL that can be used to access the given storage file.
323+// URL returns a URL that can be used to access the given storage file.
324 func (s *localStorage) URL(name string) (string, error) {
325- return s.baseURL + name, nil
326+ return fmt.Sprintf("http://%s/%s", s.addr, name), nil
327+}
328+
329+// modURL returns a URL that can be used to modify the given storage file.
330+func (s *localStorage) modURL(name string) (string, error) {
331+ if s.client == http.DefaultClient {
332+ return s.URL(name)
333+ }
334+ s.httpsBaseURLOnce.Do(func() {
335+ s.httpsBaseURL, s.httpsBaseURLError = s.getHTTPSBaseURL()
336+ })
337+ if s.httpsBaseURLError != nil {
338+ return "", s.httpsBaseURLError
339+ }
340+ v := url.Values{}
341+ v.Set("authkey", s.authkey)
342+ return fmt.Sprintf("%s%s?%s", s.httpsBaseURL, name, v.Encode()), nil
343 }
344
345 // ConsistencyStrategy is specified in the StorageReader interface.
346@@ -137,7 +166,7 @@
347 // Put reads from r and writes to the given storage file.
348 // The length must be set to the total length of the file.
349 func (s *localStorage) Put(name string, r io.Reader, length int64) error {
350- url, err := s.URL(name)
351+ url, err := s.modURL(name)
352 if err != nil {
353 return err
354 }
355@@ -168,7 +197,7 @@
356 // storage. It should not return an error if the file does
357 // not exist.
358 func (s *localStorage) Remove(name string) error {
359- url, err := s.URL(name)
360+ url, err := s.modURL(name)
361 if err != nil {
362 return err
363 }
364
365=== modified file 'environs/httpstorage/storage_test.go'
366--- environs/httpstorage/storage_test.go 2013-09-23 10:05:53 +0000
367+++ environs/httpstorage/storage_test.go 2013-10-02 05:48:33 +0000
368@@ -25,12 +25,9 @@
369 var _ = gc.Suite(&storageSuite{})
370
371 func (s *storageSuite) TestClientTLS(c *gc.C) {
372- caCertPEM := []byte(coretesting.CACert)
373- caKeyPEM := []byte(coretesting.CAKey)
374-
375- listener, _, storageDir := startServerTLS(c, caCertPEM, caKeyPEM)
376+ listener, _, storageDir := startServerTLS(c)
377 defer listener.Close()
378- stor, err := httpstorage.ClientTLS(listener.Addr().String(), caCertPEM, caKeyPEM)
379+ stor, err := httpstorage.ClientTLS(listener.Addr().String(), []byte(coretesting.CACert), testAuthkey)
380 c.Assert(err, gc.IsNil)
381
382 data := []byte("hello")
383@@ -41,13 +38,37 @@
384 c.Assert(names, gc.DeepEquals, []string{"filename"})
385 checkFileHasContents(c, stor, "filename", data)
386
387- // Now try Put, Remove and RemoveAll.
388+ // Put, Remove and RemoveAll should all succeed.
389 checkPutFile(c, stor, "filenamethesecond", data)
390 checkFileHasContents(c, stor, "filenamethesecond", data)
391 c.Assert(stor.Remove("filenamethesecond"), gc.IsNil)
392 c.Assert(stor.RemoveAll(), gc.IsNil)
393 }
394
395+func (s *storageSuite) TestClientTLSInvalidAuth(c *gc.C) {
396+ listener, _, storageDir := startServerTLS(c)
397+ defer listener.Close()
398+ const invalidAuthkey = testAuthkey + "!"
399+ stor, err := httpstorage.ClientTLS(listener.Addr().String(), []byte(coretesting.CACert), invalidAuthkey)
400+ c.Assert(err, gc.IsNil)
401+
402+ // Get and List should succeed.
403+ data := []byte("hello")
404+ err = ioutil.WriteFile(filepath.Join(storageDir, "filename"), data, 0644)
405+ c.Assert(err, gc.IsNil)
406+ names, err := storage.List(stor, "filename")
407+ c.Assert(err, gc.IsNil)
408+ c.Assert(names, gc.DeepEquals, []string{"filename"})
409+ checkFileHasContents(c, stor, "filename", data)
410+
411+ // Put, Remove and RemoveAll should all fail.
412+ const authErrorPattern = ".*401 Unauthorized"
413+ err = putFile(c, stor, "filenamethesecond", data)
414+ c.Assert(err, gc.ErrorMatches, authErrorPattern)
415+ c.Assert(stor.Remove("filenamethesecond"), gc.ErrorMatches, authErrorPattern)
416+ c.Assert(stor.RemoveAll(), gc.ErrorMatches, authErrorPattern)
417+}
418+
419 func (s *storageSuite) TestList(c *gc.C) {
420 listener, _, _ := startServer(c)
421 defer listener.Close()
422@@ -80,7 +101,6 @@
423 storage2 := httpstorage.Client(listener.Addr().String())
424 for _, name := range names {
425 checkFileHasContents(c, storage2, name, []byte(name))
426- checkFileURLHasContents(c, storage2, name, []byte(name))
427 }
428
429 // remove the first file and check that the others remain.
430@@ -128,12 +148,17 @@
431 return nil
432 }
433
434-func checkPutFile(c *gc.C, stor storage.StorageWriter, name string, contents []byte) {
435+func putFile(c *gc.C, stor storage.StorageWriter, name string, contents []byte) error {
436 c.Logf("check putting file %s ...", name)
437 reader := &readerWithClose{bytes.NewBuffer(contents), false}
438 err := stor.Put(name, reader, int64(len(contents)))
439- c.Assert(err, gc.IsNil)
440 c.Assert(reader.closeCalled, jc.IsFalse)
441+ return err
442+}
443+
444+func checkPutFile(c *gc.C, stor storage.StorageWriter, name string, contents []byte) {
445+ err := putFile(c, stor, name, contents)
446+ c.Assert(err, gc.IsNil)
447 }
448
449 func checkFileDoesNotExist(c *gc.C, stor storage.StorageReader, name string) {
450@@ -147,19 +172,15 @@
451 c.Assert(err, gc.IsNil)
452 c.Check(r, gc.NotNil)
453 defer r.Close()
454-
455 data, err := ioutil.ReadAll(r)
456 c.Check(err, gc.IsNil)
457 c.Check(data, gc.DeepEquals, contents)
458-}
459
460-func checkFileURLHasContents(c *gc.C, stor storage.StorageReader, name string, contents []byte) {
461 url, err := stor.URL(name)
462 c.Assert(err, gc.IsNil)
463-
464 resp, err := http.Get(url)
465 c.Assert(err, gc.IsNil)
466- data, err := ioutil.ReadAll(resp.Body)
467+ data, err = ioutil.ReadAll(resp.Body)
468 c.Assert(err, gc.IsNil)
469 defer resp.Body.Close()
470 c.Assert(resp.StatusCode, gc.Equals, http.StatusOK, gc.Commentf("error response: %s", data))
471
472=== modified file 'environs/manual/bootstrap.go'
473--- environs/manual/bootstrap.go 2013-10-02 05:18:02 +0000
474+++ environs/manual/bootstrap.go 2013-10-02 05:48:33 +0000
475@@ -7,7 +7,6 @@
476 "errors"
477 "fmt"
478
479- "launchpad.net/juju-core/agent"
480 "launchpad.net/juju-core/environs"
481 envtools "launchpad.net/juju-core/environs/tools"
482 "launchpad.net/juju-core/instance"
483@@ -107,11 +106,9 @@
484 tools.URL = fmt.Sprintf("file://%s/%s", storageDir, toolsStorageName)
485
486 // Add the local storage configuration.
487- agentEnv := map[string]string{
488- agent.StorageAddr: args.Environ.StorageAddr(),
489- agent.StorageDir: storageDir,
490- agent.SharedStorageAddr: args.Environ.SharedStorageAddr(),
491- agent.SharedStorageDir: args.Environ.SharedStorageDir(),
492+ agentEnv, err := localstorage.StoreConfig(args.Environ)
493+ if err != nil {
494+ return err
495 }
496
497 // Finally, provision the machine agent.
498
499=== modified file 'provider/null/config.go'
500--- provider/null/config.go 2013-09-24 02:27:12 +0000
501+++ provider/null/config.go 2013-10-02 05:48:33 +0000
502@@ -16,11 +16,13 @@
503 "bootstrap-user": schema.String(),
504 "storage-listen-ip": schema.String(),
505 "storage-port": schema.Int(),
506+ "storage-auth-key": schema.String(),
507 }
508 configDefaults = schema.Defaults{
509 "bootstrap-user": "",
510 "storage-listen-ip": "",
511 "storage-port": 8040,
512+ "storage-auth-key": schema.Omit,
513 }
514 )
515
516@@ -57,6 +59,10 @@
517 return int(c.attrs["storage-port"].(int64))
518 }
519
520+func (c *environConfig) storageAuthKey() string {
521+ return c.attrs["storage-auth-key"].(string)
522+}
523+
524 // storageAddr returns an address for connecting to the
525 // bootstrap machine's localstorage.
526 func (c *environConfig) storageAddr() string {
527
528=== modified file 'provider/null/config_test.go'
529--- provider/null/config_test.go 2013-09-24 03:02:54 +0000
530+++ provider/null/config_test.go 2013-10-02 05:48:33 +0000
531@@ -28,9 +28,10 @@
532
533 func minimalConfigValues() map[string]interface{} {
534 return map[string]interface{}{
535- "name": "test",
536- "type": provider.Null,
537- "bootstrap-host": "hostname",
538+ "name": "test",
539+ "type": provider.Null,
540+ "bootstrap-host": "hostname",
541+ "storage-auth-key": "whatever",
542 // While the ca-cert bits aren't entirely minimal, they avoid the need
543 // to set up a fake home.
544 "ca-cert": coretesting.CACert,
545
546=== modified file 'provider/null/environ.go'
547--- provider/null/environ.go 2013-10-02 05:18:02 +0000
548+++ provider/null/environ.go 2013-10-02 05:48:33 +0000
549@@ -5,9 +5,12 @@
550
551 import (
552 "errors"
553+ "net"
554 "path"
555 "sync"
556
557+ "launchpad.net/loggo"
558+
559 "launchpad.net/juju-core/constraints"
560 "launchpad.net/juju-core/environs"
561 "launchpad.net/juju-core/environs/cloudinit"
562@@ -23,6 +26,7 @@
563 "launchpad.net/juju-core/state"
564 "launchpad.net/juju-core/state/api"
565 "launchpad.net/juju-core/tools"
566+ "launchpad.net/juju-core/worker/localstorage"
567 )
568
569 const (
570@@ -39,6 +43,8 @@
571 storageTmpSubdir = "storage-tmp"
572 )
573
574+var logger = loggo.GetLogger("juju.provider.null")
575+
576 type nullEnviron struct {
577 cfg *environConfig
578 cfgmutex sync.Mutex
579@@ -160,7 +166,20 @@
580 if e.bootstrapStorage != nil {
581 return e.bootstrapStorage
582 }
583- return httpstorage.Client(e.envConfig().storageAddr())
584+ caCertPEM, caKeyPEM := e.StorageCACert(), e.StorageCAKey()
585+ if caCertPEM != nil && caKeyPEM != nil {
586+ authkey := e.StorageAuthKey()
587+ storage, err := httpstorage.ClientTLS(e.envConfig().storageAddr(), caCertPEM, authkey)
588+ if err != nil {
589+ // Should be impossible, since ca-cert will always be validated.
590+ logger.Errorf("initialising HTTPS storage failed: %v", err)
591+ } else {
592+ return storage
593+ }
594+ } else {
595+ logger.Errorf("missing CA cert or private key")
596+ }
597+ return nil
598 }
599
600 func (e *nullEnviron) PublicStorage() storage.StorageReader {
601@@ -202,3 +221,34 @@
602 func (e *nullEnviron) SharedStorageDir() string {
603 return ""
604 }
605+
606+func (e *nullEnviron) StorageCACert() []byte {
607+ if bytes, ok := e.envConfig().CACert(); ok {
608+ return bytes
609+ }
610+ return nil
611+}
612+
613+func (e *nullEnviron) StorageCAKey() []byte {
614+ if bytes, ok := e.envConfig().CAPrivateKey(); ok {
615+ return bytes
616+ }
617+ return nil
618+}
619+
620+func (e *nullEnviron) StorageHostnames() []string {
621+ cfg := e.envConfig()
622+ hostnames := []string{cfg.bootstrapHost()}
623+ if ip := net.ParseIP(cfg.storageListenIPAddress()); ip != nil {
624+ if !ip.IsUnspecified() {
625+ hostnames = append(hostnames, ip.String())
626+ }
627+ }
628+ return hostnames
629+}
630+
631+func (e *nullEnviron) StorageAuthKey() string {
632+ return e.envConfig().storageAuthKey()
633+}
634+
635+var _ localstorage.LocalTLSStorageConfig = (*nullEnviron)(nil)
636
637=== modified file 'provider/null/provider.go'
638--- provider/null/provider.go 2013-09-26 11:21:52 +0000
639+++ provider/null/provider.go 2013-10-02 05:48:33 +0000
640@@ -99,12 +99,19 @@
641 ## network interfaces.
642 # storage-listen-ip:
643 # storage-port: 8040
644+ storage-auth-key: {{rand}}
645
646 `
647 }
648
649-func (_ nullProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) {
650- return make(map[string]string), nil
651+func (p nullProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) {
652+ envConfig, err := p.validate(cfg, nil)
653+ if err != nil {
654+ return nil, err
655+ }
656+ attrs := make(map[string]string)
657+ attrs["storage-auth-key"] = envConfig.storageAuthKey()
658+ return attrs, nil
659 }
660
661 func (_ nullProvider) PublicAddress() (string, error) {
662
663=== modified file 'worker/localstorage/config.go'
664--- worker/localstorage/config.go 2013-09-16 02:03:12 +0000
665+++ worker/localstorage/config.go 2013-10-02 05:48:33 +0000
666@@ -3,6 +3,27 @@
667
668 package localstorage
669
670+import (
671+ "launchpad.net/goyaml"
672+
673+ "launchpad.net/juju-core/agent"
674+)
675+
676+const (
677+ // TODO(axw) 2013-09-25 bug #1230131
678+ // Move these variables out of agent when we can do upgrades in
679+ // the right place. In this case, the local provider should do
680+ // the envvar-to-agent.conf migration.
681+ StorageDir = agent.StorageDir
682+ StorageAddr = agent.StorageAddr
683+ SharedStorageDir = agent.SharedStorageDir
684+ SharedStorageAddr = agent.SharedStorageAddr
685+ StorageCACert = "StorageCACert"
686+ StorageCAKey = "StorageCAKey"
687+ StorageHostnames = "StorageHostnames"
688+ StorageAuthKey = "StorageAuthKey"
689+)
690+
691 // LocalStorageConfig is an interface that, if implemented, may be used
692 // to configure a machine agent for use with the localstorage worker in
693 // this package.
694@@ -12,3 +33,94 @@
695 SharedStorageDir() string
696 SharedStorageAddr() string
697 }
698+
699+// LocalTLSStorageConfig is an interface that extends LocalStorageConfig
700+// to support serving storage over TLS.
701+type LocalTLSStorageConfig interface {
702+ LocalStorageConfig
703+
704+ // StorageCACert is the CA certificate in PEM format.
705+ StorageCACert() []byte
706+
707+ // StorageCAKey is the CA private key in PEM format.
708+ StorageCAKey() []byte
709+
710+ // StorageHostnames is the set of hostnames that will
711+ // be assigned to the storage server's certificate.
712+ StorageHostnames() []string
713+
714+ // StorageAuthKey is the key that clients must present
715+ // to perform modifying operations.
716+ StorageAuthKey() string
717+}
718+
719+type config struct {
720+ storageDir string
721+ storageAddr string
722+ sharedStorageDir string
723+ sharedStorageAddr string
724+ caCertPEM []byte
725+ caKeyPEM []byte
726+ hostnames []string
727+ authkey string
728+}
729+
730+// StoreConfig takes a LocalStorageConfig (or derivative interface),
731+// and stores it in a map[string]string suitable for updating an
732+// agent.Config's key/value map.
733+func StoreConfig(storageConfig LocalStorageConfig) (map[string]string, error) {
734+ kv := make(map[string]string)
735+ kv[StorageDir] = storageConfig.StorageDir()
736+ kv[StorageAddr] = storageConfig.StorageAddr()
737+ kv[SharedStorageDir] = storageConfig.SharedStorageDir()
738+ kv[SharedStorageAddr] = storageConfig.SharedStorageAddr()
739+ if tlsConfig, ok := storageConfig.(LocalTLSStorageConfig); ok {
740+ if authkey := tlsConfig.StorageAuthKey(); authkey != "" {
741+ kv[StorageAuthKey] = authkey
742+ }
743+ if cert := tlsConfig.StorageCACert(); cert != nil {
744+ kv[StorageCACert] = string(cert)
745+ }
746+ if key := tlsConfig.StorageCAKey(); key != nil {
747+ kv[StorageCAKey] = string(key)
748+ }
749+ if hostnames := tlsConfig.StorageHostnames(); len(hostnames) > 0 {
750+ data, err := goyaml.Marshal(hostnames)
751+ if err != nil {
752+ return nil, err
753+ }
754+ kv[StorageHostnames] = string(data)
755+ }
756+ }
757+ return kv, nil
758+}
759+
760+func loadConfig(agentConfig agent.Config) (*config, error) {
761+ config := &config{
762+ storageDir: agentConfig.Value(StorageDir),
763+ storageAddr: agentConfig.Value(StorageAddr),
764+ sharedStorageDir: agentConfig.Value(SharedStorageDir),
765+ sharedStorageAddr: agentConfig.Value(SharedStorageAddr),
766+ authkey: agentConfig.Value(StorageAuthKey),
767+ }
768+
769+ caCertPEM := agentConfig.Value(StorageCACert)
770+ if len(caCertPEM) > 0 {
771+ config.caCertPEM = []byte(caCertPEM)
772+ }
773+
774+ caKeyPEM := agentConfig.Value(StorageCAKey)
775+ if len(caKeyPEM) > 0 {
776+ config.caKeyPEM = []byte(caKeyPEM)
777+ }
778+
779+ hostnames := agentConfig.Value(StorageHostnames)
780+ if len(hostnames) > 0 {
781+ err := goyaml.Unmarshal([]byte(hostnames), &config.hostnames)
782+ if err != nil {
783+ return nil, err
784+ }
785+ }
786+
787+ return config, nil
788+}
789
790=== added file 'worker/localstorage/config_test.go'
791--- worker/localstorage/config_test.go 1970-01-01 00:00:00 +0000
792+++ worker/localstorage/config_test.go 2013-10-02 05:48:33 +0000
793@@ -0,0 +1,132 @@
794+// Copyright 2013 Canonical Ltd.
795+// Licensed under the AGPLv3, see LICENCE file for details.
796+
797+package localstorage_test
798+
799+import (
800+ stdtesting "testing"
801+
802+ gc "launchpad.net/gocheck"
803+ "launchpad.net/goyaml"
804+
805+ "launchpad.net/juju-core/worker/localstorage"
806+)
807+
808+type configSuite struct{}
809+
810+var _ = gc.Suite(&configSuite{})
811+
812+func TestPackage(t *stdtesting.T) {
813+ gc.TestingT(t)
814+}
815+
816+type localStorageConfig struct {
817+ storageDir string
818+ storageAddr string
819+ sharedStorageDir string
820+ sharedStorageAddr string
821+}
822+
823+func (c *localStorageConfig) StorageDir() string {
824+ return c.storageDir
825+}
826+
827+func (c *localStorageConfig) StorageAddr() string {
828+ return c.storageAddr
829+}
830+
831+func (c *localStorageConfig) SharedStorageDir() string {
832+ return c.sharedStorageDir
833+}
834+
835+func (c *localStorageConfig) SharedStorageAddr() string {
836+ return c.sharedStorageAddr
837+}
838+
839+type localTLSStorageConfig struct {
840+ localStorageConfig
841+ caCertPEM []byte
842+ caKeyPEM []byte
843+ hostnames []string
844+ authkey string
845+}
846+
847+func (c *localTLSStorageConfig) StorageCACert() []byte {
848+ return c.caCertPEM
849+}
850+
851+func (c *localTLSStorageConfig) StorageCAKey() []byte {
852+ return c.caKeyPEM
853+}
854+
855+func (c *localTLSStorageConfig) StorageHostnames() []string {
856+ return c.hostnames
857+}
858+
859+func (c *localTLSStorageConfig) StorageAuthKey() string {
860+ return c.authkey
861+}
862+
863+func (*configSuite) TestStoreConfig(c *gc.C) {
864+ var config localStorageConfig
865+ m, err := localstorage.StoreConfig(&config)
866+ c.Assert(err, gc.IsNil)
867+ c.Assert(m, gc.DeepEquals, map[string]string{
868+ localstorage.StorageDir: "",
869+ localstorage.StorageAddr: "",
870+ localstorage.SharedStorageDir: "",
871+ localstorage.SharedStorageAddr: "",
872+ })
873+
874+ config.storageDir = "a"
875+ config.storageAddr = "b"
876+ config.sharedStorageDir = "c"
877+ config.sharedStorageAddr = "d"
878+ m, err = localstorage.StoreConfig(&config)
879+ c.Assert(err, gc.IsNil)
880+ c.Assert(m, gc.DeepEquals, map[string]string{
881+ localstorage.StorageDir: config.storageDir,
882+ localstorage.StorageAddr: config.storageAddr,
883+ localstorage.SharedStorageDir: config.sharedStorageDir,
884+ localstorage.SharedStorageAddr: config.sharedStorageAddr,
885+ })
886+}
887+
888+func (*configSuite) TestStoreConfigTLS(c *gc.C) {
889+ var config localTLSStorageConfig
890+ m, err := localstorage.StoreConfig(&config)
891+ c.Assert(err, gc.IsNil)
892+ c.Assert(m, gc.DeepEquals, map[string]string{
893+ localstorage.StorageDir: "",
894+ localstorage.StorageAddr: "",
895+ localstorage.SharedStorageDir: "",
896+ localstorage.SharedStorageAddr: "",
897+ })
898+
899+ config.storageDir = "a"
900+ config.storageAddr = "b"
901+ config.sharedStorageDir = "c"
902+ config.sharedStorageAddr = "d"
903+ config.caCertPEM = []byte("heyhey")
904+ config.caKeyPEM = []byte("hoho")
905+ config.hostnames = []string{"easy", "as", "1.2.3"}
906+ config.authkey = "password"
907+ m, err = localstorage.StoreConfig(&config)
908+ c.Assert(err, gc.IsNil)
909+ c.Assert(m, gc.DeepEquals, map[string]string{
910+ localstorage.StorageDir: config.storageDir,
911+ localstorage.StorageAddr: config.storageAddr,
912+ localstorage.SharedStorageDir: config.sharedStorageDir,
913+ localstorage.SharedStorageAddr: config.sharedStorageAddr,
914+ localstorage.StorageCACert: string(config.caCertPEM),
915+ localstorage.StorageCAKey: string(config.caKeyPEM),
916+ localstorage.StorageHostnames: mustMarshalYAML(c, config.hostnames),
917+ localstorage.StorageAuthKey: config.authkey,
918+ })
919+}
920+
921+func mustMarshalYAML(c *gc.C, v interface{}) string {
922+ data, err := goyaml.Marshal(v)
923+ c.Assert(err, gc.IsNil)
924+ return string(data)
925+}
926
927=== modified file 'worker/localstorage/worker.go'
928--- worker/localstorage/worker.go 2013-09-20 04:41:12 +0000
929+++ worker/localstorage/worker.go 2013-10-02 05:48:33 +0000
930@@ -4,6 +4,8 @@
931 package localstorage
932
933 import (
934+ "net"
935+
936 "launchpad.net/loggo"
937 "launchpad.net/tomb"
938
939@@ -39,40 +41,53 @@
940 return s.tomb.Wait()
941 }
942
943+func (s *storageWorker) serveStorage(storageAddr, storageDir string, config *config) (net.Listener, error) {
944+ authenticated := len(config.caCertPEM) > 0 && len(config.caKeyPEM) > 0
945+ scheme := "http://"
946+ if authenticated {
947+ scheme = "https://"
948+ }
949+ logger.Infof("serving storage from %s to %s%s", storageDir, scheme, storageAddr)
950+ storage, err := filestorage.NewFileStorageWriter(storageDir, filestorage.UseDefaultTmpDir)
951+ if err != nil {
952+ return nil, err
953+ }
954+ if authenticated {
955+ return httpstorage.ServeTLS(
956+ storageAddr,
957+ storage,
958+ config.caCertPEM,
959+ config.caKeyPEM,
960+ config.hostnames,
961+ config.authkey,
962+ )
963+ }
964+ return httpstorage.Serve(storageAddr, storage)
965+}
966+
967 func (s *storageWorker) waitForDeath() error {
968- storageDir := s.config.Value(agent.StorageDir)
969- storageAddr := s.config.Value(agent.StorageAddr)
970- logger.Infof("serving %s on %s", storageDir, storageAddr)
971+ config, err := loadConfig(s.config)
972+ if err != nil {
973+ logger.Errorf("error loading config: %v", err)
974+ return err
975+ }
976
977- storage, err := filestorage.NewFileStorageWriter(storageDir, filestorage.UseDefaultTmpDir)
978- if err != nil {
979- logger.Errorf("error with local storage: %v", err)
980- return err
981- }
982- storageListener, err := httpstorage.Serve(storageAddr, storage)
983+ storageListener, err := s.serveStorage(config.storageAddr, config.storageDir, config)
984 if err != nil {
985 logger.Errorf("error with local storage: %v", err)
986 return err
987 }
988 defer storageListener.Close()
989
990- sharedStorageDir := s.config.Value(agent.SharedStorageDir)
991- sharedStorageAddr := s.config.Value(agent.SharedStorageAddr)
992- if sharedStorageAddr != "" && sharedStorageDir != "" {
993- logger.Infof("serving %s on %s", sharedStorageDir, sharedStorageAddr)
994- sharedStorage, err := filestorage.NewFileStorageWriter(sharedStorageDir, filestorage.UseDefaultTmpDir)
995- if err != nil {
996- logger.Errorf("error with local storage: %v", err)
997- return err
998- }
999- sharedStorageListener, err := httpstorage.Serve(sharedStorageAddr, sharedStorage)
1000+ if config.sharedStorageAddr != "" && config.sharedStorageDir != "" {
1001+ sharedStorageListener, err := s.serveStorage(config.sharedStorageAddr, config.sharedStorageDir, config)
1002 if err != nil {
1003 logger.Errorf("error with local storage: %v", err)
1004 return err
1005 }
1006 defer sharedStorageListener.Close()
1007 } else {
1008- logger.Infof("no shared storage: dir=%q addr=%q", sharedStorageDir, sharedStorageAddr)
1009+ logger.Infof("no shared storage: dir=%q addr=%q", config.sharedStorageDir, config.sharedStorageAddr)
1010 }
1011
1012 logger.Infof("storage routines started, awaiting death")

Subscribers

People subscribed via source and target branches

to status/vote changes: