Merge lp:~axwalk/juju-core/1225916-httpstroage-authentication 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: 1878
Proposed branch: lp:~axwalk/juju-core/1225916-httpstroage-authentication
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 654 lines (+283/-30)
8 files modified
cert/cert.go (+21/-3)
cert/cert_test.go (+44/-6)
environs/config/config.go (+2/-1)
environs/httpstorage/backend.go (+61/-1)
environs/httpstorage/backend_test.go (+79/-8)
environs/httpstorage/storage.go (+44/-9)
environs/httpstorage/storage_test.go (+30/-1)
testing/cert.go (+2/-1)
To merge this branch: bzr merge lp:~axwalk/juju-core/1225916-httpstroage-authentication
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+187152@code.launchpad.net

Commit message

environs/httpstorage: authentication support

This change to httpstorage enables authentication,
where authentication implies authorisation.
For an authenticating httpstorage, authentication
is required only for Put/Remove* methods; List/Get
will work unauthenticated.

Authentication is implemented by a client providing
a certificate signed by the CA, where the CA is
previously agreed upon.

There will be a followup branch which enables this
for the null provider, via additions to the
worker/localstorage.LocalStorageConfig interface,
and storage CA certificate generation at bootstrap
time.

NOTE: one caveat is that wget et al. will need to
skip certificate validation.

Fixes #1225916

https://codereview.appspot.com/13832045/

Description of the change

environs/httpstorage: authentication support

This change to httpstorage enables authentication,
where authentication implies authorisation.
For an authenticating httpstorage, authentication
is required only for Put/Remove* methods; List/Get
will work unauthenticated.

Authentication is implemented by a client providing
a certificate signed by the CA, where the CA is
previously agreed upon.

There will be a followup branch which enables this
for the null provider, via additions to the
worker/localstorage.LocalStorageConfig interface,
and storage CA certificate generation at bootstrap
time.

NOTE: one caveat is that wget et al. will need to
skip certificate validation.

Fixes #1225916

https://codereview.appspot.com/13832045/

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

Reviewers: mp+187152_code.launchpad.net,

Message:
Please take a look.

Description:
environs/httpstorage: authentication support

This change to httpstorage enables authentication,
where authentication implies authorisation.
For an authenticating httpstorage, authentication
is required only for Put/Remove* methods; List/Get
will work unauthenticated.

Authentication is implemented by a client providing
a certificate signed by the CA, where the CA is
previously agreed upon.

There will be a followup branch which enables this
for the null provider, via additions to the
worker/localstorage.LocalStorageConfig interface,
and storage CA certificate generation at bootstrap
time.

NOTE: one caveat is that wget et al. will need to
skip certificate validation.

Fixes #1225916

https://code.launchpad.net/~axwalk/juju-core/1225916-httpstroage-authentication/+merge/187152

(do not edit description out of merge proposal)

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

Revision history for this message
William Reade (fwereade) wrote :

message: Lovely; LGTM. It makes me think a bit though: AFAIR the only
clients that need authentication will be the CLI and the manager nodes,
and both of those do already have suitable storage-manipulation
pathways.

So, if cert distribution proves to be a hassle, we can in fact fall back
to other storage mechanisms without losing functionality (and we still
get the security we care about). Handy if we need it.

https://codereview.appspot.com/13832045/

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

On 2013/09/24 13:33:06, fwereade wrote:
> message: Lovely; LGTM. It makes me think a bit though: AFAIR the only
clients
> that need authentication will be the CLI and the manager nodes, and
both of
> those do already have suitable storage-manipulation pathways.

What do you mean by "suitable storage-manipulation pathways"?

> So, if cert distribution proves to be a hassle, we can in fact fall
back to
> other storage mechanisms without losing functionality (and we still
get the
> security we care about). Handy if we need it.

Not really following. What are the other storage mechanisms that can be
fallen back on?

https://codereview.appspot.com/13832045/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cert/cert.go'
2--- cert/cert.go 2013-08-02 14:04:05 +0000
3+++ cert/cert.go 2013-09-25 02:37:59 +0000
4@@ -14,6 +14,7 @@
5 "errors"
6 "fmt"
7 "math/big"
8+ "net"
9 "time"
10 )
11
12@@ -114,9 +115,18 @@
13 return certPEM, keyPEM, nil
14 }
15
16-// NewServer generates a certificate/key pair suitable for use by a
17-// server for an environment with the given name.
18-func NewServer(envName string, caCertPEM, caKeyPEM []byte, expiry time.Time) (certPEM, keyPEM []byte, err error) {
19+// NewServer generates a certificate/key pair suitable for use by a server.
20+func NewServer(caCertPEM, caKeyPEM []byte, expiry time.Time, hostnames []string) (certPEM, keyPEM []byte, err error) {
21+ return newLeaf(caCertPEM, caKeyPEM, expiry, hostnames, nil)
22+}
23+
24+// NewClient generates a certificate/key pair suitable for client authentication.
25+func NewClient(caCertPEM, caKeyPEM []byte, expiry time.Time) (certPEM, keyPEM []byte, err error) {
26+ return newLeaf(caCertPEM, caKeyPEM, expiry, nil, []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})
27+}
28+
29+// newLeaf generates a certificate/key pair suitable for use by a leaf node.
30+func newLeaf(caCertPEM, caKeyPEM []byte, expiry time.Time, hostnames []string, extKeyUsage []x509.ExtKeyUsage) (certPEM, keyPEM []byte, err error) {
31 tlsCert, err := tls.X509KeyPair(caCertPEM, caKeyPEM)
32 if err != nil {
33 return nil, nil, err
34@@ -153,6 +163,14 @@
35
36 SubjectKeyId: bigIntHash(key.N),
37 KeyUsage: x509.KeyUsageDataEncipherment,
38+ ExtKeyUsage: extKeyUsage,
39+ }
40+ for _, hostname := range hostnames {
41+ if ip := net.ParseIP(hostname); ip != nil {
42+ template.IPAddresses = append(template.IPAddresses, ip)
43+ } else {
44+ template.DNSNames = append(template.DNSNames, hostname)
45+ }
46 }
47 certDER, err := x509.CreateCertificate(rand.Reader, template, caCert, &key.PublicKey, caKey)
48 if err != nil {
49
50=== modified file 'cert/cert_test.go'
51--- cert/cert_test.go 2013-09-13 14:48:13 +0000
52+++ cert/cert_test.go 2013-09-25 02:37:59 +0000
53@@ -76,12 +76,12 @@
54 caCert, _, err := cert.ParseCertAndKey(caCertPEM, caKeyPEM)
55 c.Assert(err, gc.IsNil)
56
57- srvCertPEM, srvKeyPEM, err := cert.NewServer("juju test", caCertPEM, caKeyPEM, expiry)
58+ var noHostnames []string
59+ srvCertPEM, srvKeyPEM, err := cert.NewServer(caCertPEM, caKeyPEM, expiry, noHostnames)
60 c.Assert(err, gc.IsNil)
61
62 srvCert, srvKey, err := cert.ParseCertAndKey(srvCertPEM, srvKeyPEM)
63 c.Assert(err, gc.IsNil)
64- c.Assert(err, gc.IsNil)
65 c.Assert(srvCert.Subject.CommonName, gc.Equals, "*")
66 c.Assert(srvCert.NotAfter.Equal(expiry), gc.Equals, true)
67 c.Assert(srvCert.BasicConstraintsValid, gc.Equals, false)
68@@ -90,6 +90,41 @@
69 checkTLSConnection(c, caCert, srvCert, srvKey)
70 }
71
72+func (certSuite) TestNewServerHostnames(c *gc.C) {
73+ type test struct {
74+ hostnames []string
75+ expectedDNSNames []string
76+ expectedIPAddresses []net.IP
77+ }
78+ tests := []test{{
79+ []string{},
80+ nil,
81+ nil,
82+ }, {
83+ []string{"example.com"},
84+ []string{"example.com"},
85+ nil,
86+ }, {
87+ []string{"example.com", "127.0.0.1"},
88+ []string{"example.com"},
89+ []net.IP{net.IPv4(127, 0, 0, 1).To4()},
90+ }, {
91+ []string{"::1"},
92+ nil,
93+ []net.IP{net.IPv6loopback},
94+ }}
95+ for i, t := range tests {
96+ c.Logf("test %d: %v", i, t.hostnames)
97+ expiry := roundTime(time.Now().AddDate(1, 0, 0))
98+ srvCertPEM, srvKeyPEM, err := cert.NewServer(caCertPEM, caKeyPEM, expiry, t.hostnames)
99+ c.Assert(err, gc.IsNil)
100+ srvCert, _, err := cert.ParseCertAndKey(srvCertPEM, srvKeyPEM)
101+ c.Assert(err, gc.IsNil)
102+ c.Assert(srvCert.DNSNames, gc.DeepEquals, t.expectedDNSNames)
103+ c.Assert(srvCert.IPAddresses, gc.DeepEquals, t.expectedIPAddresses)
104+ }
105+}
106+
107 func (certSuite) TestWithNonUTCExpiry(c *gc.C) {
108 expiry, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", "2012-11-28 15:53:57 +0100 CET")
109 c.Assert(err, gc.IsNil)
110@@ -98,14 +133,16 @@
111 c.Assert(err, gc.IsNil)
112 c.Assert(xcert.NotAfter.Equal(expiry), gc.Equals, true)
113
114- certPEM, _, err = cert.NewServer("foo", certPEM, keyPEM, expiry)
115+ var noHostnames []string
116+ certPEM, _, err = cert.NewServer(certPEM, keyPEM, expiry, noHostnames)
117 xcert, err = cert.ParseCert(certPEM)
118 c.Assert(err, gc.IsNil)
119 c.Assert(xcert.NotAfter.Equal(expiry), gc.Equals, true)
120 }
121
122 func (certSuite) TestNewServerWithInvalidCert(c *gc.C) {
123- srvCert, srvKey, err := cert.NewServer("foo", nonCACert, nonCAKey, time.Now())
124+ var noHostnames []string
125+ srvCert, srvKey, err := cert.NewServer(nonCACert, nonCAKey, time.Now(), noHostnames)
126 c.Check(srvCert, gc.IsNil)
127 c.Check(srvKey, gc.IsNil)
128 c.Assert(err, gc.ErrorMatches, "CA certificate is not a valid CA")
129@@ -116,7 +153,8 @@
130 caCert, caKey, err := cert.NewCA("foo", now.Add(1*time.Minute))
131 c.Assert(err, gc.IsNil)
132
133- srvCert, _, err := cert.NewServer("foo", caCert, caKey, now.Add(3*time.Minute))
134+ var noHostnames []string
135+ srvCert, _, err := cert.NewServer(caCert, caKey, now.Add(3*time.Minute), noHostnames)
136 c.Assert(err, gc.IsNil)
137
138 err = cert.Verify(srvCert, caCert, now)
139@@ -139,7 +177,7 @@
140 err = cert.Verify(srvCert, caCert2, now)
141 c.Check(err, gc.ErrorMatches, "x509: certificate signed by unknown authority")
142
143- srvCert2, _, err := cert.NewServer("bar", caCert2, caKey2, now.Add(1*time.Minute))
144+ srvCert2, _, err := cert.NewServer(caCert2, caKey2, now.Add(1*time.Minute), noHostnames)
145 c.Assert(err, gc.IsNil)
146
147 // Check new server certificate against original CA.
148
149=== modified file 'environs/config/config.go'
150--- environs/config/config.go 2013-09-13 01:30:21 +0000
151+++ environs/config/config.go 2013-09-25 02:37:59 +0000
152@@ -610,5 +610,6 @@
153 if !hasCAKey {
154 return nil, nil, fmt.Errorf("environment configuration has no ca-private-key")
155 }
156- return cert.NewServer(cfg.Name(), caCert, caKey, time.Now().UTC().AddDate(10, 0, 0))
157+ var noHostnames []string
158+ return cert.NewServer(caCert, caKey, time.Now().UTC().AddDate(10, 0, 0), noHostnames)
159 }
160
161=== modified file 'environs/httpstorage/backend.go'
162--- environs/httpstorage/backend.go 2013-09-19 00:22:15 +0000
163+++ environs/httpstorage/backend.go 2013-09-25 02:37:59 +0000
164@@ -4,12 +4,17 @@
165 package httpstorage
166
167 import (
168+ "crypto/tls"
169+ "crypto/x509"
170+ "errors"
171 "fmt"
172 "io/ioutil"
173 "net"
174 "net/http"
175 "strings"
176+ "time"
177
178+ "launchpad.net/juju-core/cert"
179 "launchpad.net/juju-core/environs/storage"
180 )
181
182@@ -19,6 +24,7 @@
183 // storageBackend provides HTTP access to a storage object.
184 type storageBackend struct {
185 backend storage.Storage
186+ tls bool
187 }
188
189 // ServeHTTP handles the HTTP requests to the container.
190@@ -39,6 +45,13 @@
191 }
192 }
193
194+// authorised checks that either the storage does not require authorisation,
195+// or the user has negotiated TLS with a valid certificate. Authentication
196+// implies authorisation.
197+func (s *storageBackend) authorised(req *http.Request) bool {
198+ return !s.tls || len(req.TLS.PeerCertificates) > 0
199+}
200+
201 // handleGet returns a storage file to the client.
202 func (s *storageBackend) handleGet(w http.ResponseWriter, req *http.Request) {
203 readcloser, err := s.backend.Get(req.URL.Path[1:])
204@@ -72,6 +85,10 @@
205
206 // handlePut stores data from the client in the storage.
207 func (s *storageBackend) handlePut(w http.ResponseWriter, req *http.Request) {
208+ if !s.authorised(req) {
209+ http.Error(w, "unauthorised access", http.StatusUnauthorized)
210+ return
211+ }
212 if req.ContentLength < 0 {
213 http.Error(w, "missing or invalid Content-Length header", http.StatusInternalServerError)
214 return
215@@ -86,6 +103,10 @@
216
217 // handleDelete removes a file from the storage.
218 func (s *storageBackend) handleDelete(w http.ResponseWriter, req *http.Request) {
219+ if !s.authorised(req) {
220+ http.Error(w, "unauthorised access", http.StatusUnauthorized)
221+ return
222+ }
223 err := s.backend.Remove(req.URL.Path[1:])
224 if err != nil {
225 http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
226@@ -98,11 +119,50 @@
227 // requests to the given storage implementation. It returns the network
228 // listener. This can then be attached to with Client.
229 func Serve(addr string, stor storage.Storage) (net.Listener, error) {
230- backend := &storageBackend{backend: stor}
231+ return serve(addr, stor, nil)
232+}
233+
234+// ServeTLS runs a storage server on the given network address, relaying
235+// requests to the given storage implementation. The server runs a TLS
236+// listener, and verifies client certificates (if given) against the
237+// specified CA certificate. A client certificate is only required for
238+// PUT and DELETE methods.
239+//
240+// This method returns the network listener, which can then be attached
241+// to with ClientTLS.
242+func ServeTLS(addr string, stor storage.Storage, caCertPEM, caKeyPEM []byte, hostnames []string) (net.Listener, error) {
243+ expiry := time.Now().UTC().AddDate(10, 0, 0)
244+ certPEM, keyPEM, err := cert.NewServer(caCertPEM, caKeyPEM, expiry, hostnames)
245+ if err != nil {
246+ return nil, err
247+ }
248+ serverCert, err := tls.X509KeyPair(certPEM, keyPEM)
249+ if err != nil {
250+ return nil, err
251+ }
252+ caCerts := x509.NewCertPool()
253+ if !caCerts.AppendCertsFromPEM(caCertPEM) {
254+ return nil, errors.New("error adding CA certificate to pool")
255+ }
256+ config := &tls.Config{
257+ NextProtos: []string{"http/1.1"},
258+ Certificates: []tls.Certificate{serverCert},
259+ ClientAuth: tls.VerifyClientCertIfGiven,
260+ ClientCAs: caCerts,
261+ }
262+ return serve(addr, stor, config)
263+}
264+
265+func serve(addr string, stor storage.Storage, tlsConfig *tls.Config) (net.Listener, error) {
266 listener, err := net.Listen("tcp", addr)
267 if err != nil {
268 return nil, fmt.Errorf("cannot start listener: %v", err)
269 }
270+ backend := &storageBackend{backend: stor}
271+ if tlsConfig != nil {
272+ listener = tls.NewListener(listener, tlsConfig)
273+ backend.tls = true
274+ }
275 mux := http.NewServeMux()
276 mux.Handle("/", backend)
277 go http.Serve(listener, mux)
278
279=== modified file 'environs/httpstorage/backend_test.go'
280--- environs/httpstorage/backend_test.go 2013-09-20 02:53:59 +0000
281+++ environs/httpstorage/backend_test.go 2013-09-25 02:37:59 +0000
282@@ -5,6 +5,8 @@
283
284 import (
285 "bytes"
286+ "crypto/tls"
287+ "crypto/x509"
288 "fmt"
289 "io/ioutil"
290 "net"
291@@ -18,6 +20,8 @@
292
293 "launchpad.net/juju-core/environs/filestorage"
294 "launchpad.net/juju-core/environs/httpstorage"
295+ coretesting "launchpad.net/juju-core/testing"
296+ jc "launchpad.net/juju-core/testing/checkers"
297 "launchpad.net/juju-core/testing/testbase"
298 )
299
300@@ -43,6 +47,19 @@
301 return listener, fmt.Sprintf("http://%s/", listener.Addr()), dataDir
302 }
303
304+// startServerTLS starts a new TLS-based local storage server
305+// using a temporary directory and returns the listener,
306+// a base URL for the server and the directory path.
307+func startServerTLS(c *gc.C, caCertPEM, caKeyPEM []byte) (listener net.Listener, url, dataDir string) {
308+ dataDir = c.MkDir()
309+ embedded, err := filestorage.NewFileStorageWriter(dataDir, filestorage.UseDefaultTmpDir)
310+ c.Assert(err, gc.IsNil)
311+ hostnames := []string{"127.0.0.1"}
312+ listener, err = httpstorage.ServeTLS("127.0.0.1:0", embedded, caCertPEM, caKeyPEM, hostnames)
313+ c.Assert(err, gc.IsNil)
314+ return listener, fmt.Sprintf("https://%s/", listener.Addr()), dataDir
315+}
316+
317 type testCase struct {
318 name string
319 content string
320@@ -118,9 +135,12 @@
321 listener, url, dataDir := startServer(c)
322 defer listener.Close()
323 createTestData(c, dataDir)
324+ testGet(c, http.DefaultClient, url)
325+}
326
327+func testGet(c *gc.C, client *http.Client, url string) {
328 check := func(tc testCase) {
329- resp, err := http.Get(url + tc.name)
330+ resp, err := client.Get(url + tc.name)
331 c.Assert(err, gc.IsNil)
332 if tc.status != 0 {
333 c.Assert(resp.StatusCode, gc.Equals, tc.status)
334@@ -188,11 +208,13 @@
335 // Test listing file of a storage.
336 listener, url, dataDir := startServer(c)
337 defer listener.Close()
338-
339 createTestData(c, dataDir)
340+ testList(c, http.DefaultClient, url)
341+}
342
343+func testList(c *gc.C, client *http.Client, url string) {
344 check := func(tc testCase) {
345- resp, err := http.Get(url + tc.name + "*")
346+ resp, err := client.Get(url + tc.name + "*")
347 c.Assert(err, gc.IsNil)
348 if tc.status != 0 {
349 c.Assert(resp.StatusCode, gc.Equals, tc.status)
350@@ -235,20 +257,25 @@
351 // Test sending a file to the storage.
352 listener, url, dataDir := startServer(c)
353 defer listener.Close()
354-
355 createTestData(c, dataDir)
356+ testPut(c, http.DefaultClient, url, dataDir, true)
357+}
358
359+func testPut(c *gc.C, client *http.Client, url, dataDir string, authorised bool) {
360 check := func(tc testCase) {
361 req, err := http.NewRequest("PUT", url+tc.name, bytes.NewBufferString(tc.content))
362 c.Assert(err, gc.IsNil)
363 req.Header.Set("Content-Type", "application/octet-stream")
364- resp, err := http.DefaultClient.Do(req)
365+ resp, err := client.Do(req)
366 c.Assert(err, gc.IsNil)
367 if tc.status != 0 {
368 c.Assert(resp.StatusCode, gc.Equals, tc.status)
369 return
370+ } else if !authorised {
371+ c.Assert(resp.StatusCode, gc.Equals, http.StatusUnauthorized)
372+ return
373 }
374- c.Assert(resp.StatusCode, gc.Equals, 201)
375+ c.Assert(resp.StatusCode, gc.Equals, http.StatusCreated)
376
377 fp := filepath.Join(dataDir, tc.name)
378 b, err := ioutil.ReadFile(fp)
379@@ -288,9 +315,11 @@
380 // Test removing a file in the storage.
381 listener, url, dataDir := startServer(c)
382 defer listener.Close()
383-
384 createTestData(c, dataDir)
385+ testRemove(c, http.DefaultClient, url, dataDir, true)
386+}
387
388+func testRemove(c *gc.C, client *http.Client, url, dataDir string, authorised bool) {
389 check := func(tc testCase) {
390 fp := filepath.Join(dataDir, tc.name)
391 dir, _ := filepath.Split(fp)
392@@ -301,11 +330,14 @@
393
394 req, err := http.NewRequest("DELETE", url+tc.name, nil)
395 c.Assert(err, gc.IsNil)
396- resp, err := http.DefaultClient.Do(req)
397+ resp, err := client.Do(req)
398 c.Assert(err, gc.IsNil)
399 if tc.status != 0 {
400 c.Assert(resp.StatusCode, gc.Equals, tc.status)
401 return
402+ } else if !authorised {
403+ c.Assert(resp.StatusCode, gc.Equals, http.StatusUnauthorized)
404+ return
405 }
406 c.Assert(resp.StatusCode, gc.Equals, http.StatusOK)
407
408@@ -339,3 +371,42 @@
409 writeData(innerDir, "barin", "this is inner file 'barin'")
410 writeData(innerDir, "bazin", "this is inner file 'bazin'")
411 }
412+
413+func (b *backendSuite) tlsServerAndClient(c *gc.C) (client *http.Client, url, dataDir string) {
414+ caCertPEM := []byte(coretesting.CACert)
415+ caKeyPEM := []byte(coretesting.CAKey)
416+ listener, url, dataDir := startServerTLS(c, caCertPEM, caKeyPEM)
417+ b.AddCleanup(func(*gc.C) { listener.Close() })
418+ caCerts := x509.NewCertPool()
419+ c.Assert(caCerts.AppendCertsFromPEM(caCertPEM), jc.IsTrue)
420+ client = &http.Client{
421+ Transport: &http.Transport{
422+ TLSClientConfig: &tls.Config{RootCAs: caCerts},
423+ },
424+ }
425+ return client, url, dataDir
426+}
427+
428+func (b *backendSuite) TestTLSUnauthenticatedGet(c *gc.C) {
429+ client, url, dataDir := b.tlsServerAndClient(c)
430+ createTestData(c, dataDir)
431+ testGet(c, client, url)
432+}
433+
434+func (b *backendSuite) TestTLSUnauthenticatedList(c *gc.C) {
435+ client, url, dataDir := b.tlsServerAndClient(c)
436+ createTestData(c, dataDir)
437+ testList(c, client, url)
438+}
439+
440+func (b *backendSuite) TestTLSUnauthenticatedPut(c *gc.C) {
441+ client, url, dataDir := b.tlsServerAndClient(c)
442+ createTestData(c, dataDir)
443+ testPut(c, client, url, dataDir, false)
444+}
445+
446+func (b *backendSuite) TestTLSUnauthenticatedRemove(c *gc.C) {
447+ client, url, dataDir := b.tlsServerAndClient(c)
448+ createTestData(c, dataDir)
449+ testRemove(c, client, url, dataDir, false)
450+}
451
452=== modified file 'environs/httpstorage/storage.go'
453--- environs/httpstorage/storage.go 2013-09-19 00:22:15 +0000
454+++ environs/httpstorage/storage.go 2013-09-25 02:37:59 +0000
455@@ -4,29 +4,64 @@
456 package httpstorage
457
458 import (
459+ "crypto/tls"
460+ "crypto/x509"
461+ "errors"
462 "fmt"
463 "io"
464 "io/ioutil"
465 "net/http"
466 "sort"
467 "strings"
468+ "time"
469
470+ "launchpad.net/juju-core/cert"
471 "launchpad.net/juju-core/environs/storage"
472- "launchpad.net/juju-core/errors"
473+ coreerrors "launchpad.net/juju-core/errors"
474 "launchpad.net/juju-core/utils"
475 )
476
477 // storage implements the storage.Storage interface.
478 type localStorage struct {
479 baseURL string
480+ client *http.Client
481 }
482
483-// Client returns a storage object that will talk to the storage server
484-// at the given network address (see Serve)
485+// Client returns a storage object that will talk to the
486+// storage server at the given network address (see Serve)
487 func Client(addr string) storage.Storage {
488 return &localStorage{
489 baseURL: fmt.Sprintf("http://%s/", addr),
490- }
491+ client: http.DefaultClient,
492+ }
493+}
494+
495+// ClientTLS returns a storage object that will talk to the
496+// storage server at the given network address (see Serve),
497+// using TLS. The client will generate a client certificate,
498+// signed with the given CA certificate/key, to authenticate.
499+func ClientTLS(addr string, caCertPEM, caKeyPEM []byte) (storage.Storage, error) {
500+ caCerts := x509.NewCertPool()
501+ if !caCerts.AppendCertsFromPEM(caCertPEM) {
502+ return nil, errors.New("error adding CA certificate to pool")
503+ }
504+ expiry := time.Now().UTC().AddDate(10, 0, 0)
505+ clientCertPEM, clientKeyPEM, err := cert.NewClient(caCertPEM, caKeyPEM, expiry)
506+ clientCert, err := tls.X509KeyPair(clientCertPEM, clientKeyPEM)
507+ if err != nil {
508+ return nil, err
509+ }
510+ return &localStorage{
511+ baseURL: fmt.Sprintf("https://%s/", addr),
512+ client: &http.Client{
513+ Transport: &http.Transport{
514+ TLSClientConfig: &tls.Config{
515+ Certificates: []tls.Certificate{clientCert},
516+ RootCAs: caCerts,
517+ },
518+ },
519+ },
520+ }, nil
521 }
522
523 // Get opens the given storage file and returns a ReadCloser
524@@ -38,12 +73,12 @@
525 if err != nil {
526 return nil, err
527 }
528- resp, err := http.Get(url)
529+ resp, err := s.client.Get(url)
530 if err != nil {
531 return nil, err
532 }
533 if resp.StatusCode != http.StatusOK {
534- return nil, errors.NotFoundf("file %q", name)
535+ return nil, coreerrors.NotFoundf("file %q", name)
536 }
537 return resp.Body, nil
538 }
539@@ -58,7 +93,7 @@
540 if err != nil {
541 return nil, err
542 }
543- resp, err := http.Get(url + "*")
544+ resp, err := s.client.Get(url + "*")
545 if err != nil {
546 return nil, err
547 }
548@@ -118,7 +153,7 @@
549 }
550 req.Header.Set("Content-Type", "application/octet-stream")
551 req.ContentLength = length
552- resp, err := http.DefaultClient.Do(req)
553+ resp, err := s.client.Do(req)
554 if err != nil {
555 return err
556 }
557@@ -140,7 +175,7 @@
558 if err != nil {
559 return err
560 }
561- resp, err := http.DefaultClient.Do(req)
562+ resp, err := s.client.Do(req)
563 if err != nil {
564 return err
565 }
566
567=== modified file 'environs/httpstorage/storage_test.go'
568--- environs/httpstorage/storage_test.go 2013-09-19 00:22:15 +0000
569+++ environs/httpstorage/storage_test.go 2013-09-25 02:37:59 +0000
570@@ -9,12 +9,14 @@
571 "io"
572 "io/ioutil"
573 "net/http"
574+ "path/filepath"
575
576 gc "launchpad.net/gocheck"
577
578 "launchpad.net/juju-core/environs/httpstorage"
579 "launchpad.net/juju-core/environs/storage"
580 "launchpad.net/juju-core/errors"
581+ coretesting "launchpad.net/juju-core/testing"
582 jc "launchpad.net/juju-core/testing/checkers"
583 )
584
585@@ -22,6 +24,30 @@
586
587 var _ = gc.Suite(&storageSuite{})
588
589+func (s *storageSuite) TestClientTLS(c *gc.C) {
590+ caCertPEM := []byte(coretesting.CACert)
591+ caKeyPEM := []byte(coretesting.CAKey)
592+
593+ listener, _, storageDir := startServerTLS(c, caCertPEM, caKeyPEM)
594+ defer listener.Close()
595+ stor, err := httpstorage.ClientTLS(listener.Addr().String(), caCertPEM, caKeyPEM)
596+ c.Assert(err, gc.IsNil)
597+
598+ data := []byte("hello")
599+ err = ioutil.WriteFile(filepath.Join(storageDir, "filename"), data, 0644)
600+ c.Assert(err, gc.IsNil)
601+ names, err := storage.List(stor, "filename")
602+ c.Assert(err, gc.IsNil)
603+ c.Assert(names, gc.DeepEquals, []string{"filename"})
604+ checkFileHasContents(c, stor, "filename", data)
605+
606+ // Now try Put, Remove and RemoveAll.
607+ checkPutFile(c, stor, "filenamethesecond", data)
608+ checkFileHasContents(c, stor, "filenamethesecond", data)
609+ c.Assert(stor.Remove("filenamethesecond"), gc.IsNil)
610+ c.Assert(stor.RemoveAll(), gc.IsNil)
611+}
612+
613 func (s *storageSuite) TestList(c *gc.C) {
614 listener, _, _ := startServer(c)
615 defer listener.Close()
616@@ -54,6 +80,7 @@
617 storage2 := httpstorage.Client(listener.Addr().String())
618 for _, name := range names {
619 checkFileHasContents(c, storage2, name, []byte(name))
620+ checkFileURLHasContents(c, storage2, name, []byte(name))
621 }
622
623 // remove the first file and check that the others remain.
624@@ -124,13 +151,15 @@
625 data, err := ioutil.ReadAll(r)
626 c.Check(err, gc.IsNil)
627 c.Check(data, gc.DeepEquals, contents)
628+}
629
630+func checkFileURLHasContents(c *gc.C, stor storage.StorageReader, name string, contents []byte) {
631 url, err := stor.URL(name)
632 c.Assert(err, gc.IsNil)
633
634 resp, err := http.Get(url)
635 c.Assert(err, gc.IsNil)
636- data, err = ioutil.ReadAll(resp.Body)
637+ data, err := ioutil.ReadAll(resp.Body)
638 c.Assert(err, gc.IsNil)
639 defer resp.Body.Close()
640 c.Assert(resp.StatusCode, gc.Equals, http.StatusOK, gc.Commentf("error response: %s", data))
641
642=== modified file 'testing/cert.go'
643--- testing/cert.go 2013-07-09 10:32:23 +0000
644+++ testing/cert.go 2013-09-25 02:37:59 +0000
645@@ -53,7 +53,8 @@
646
647 func mustNewServer() (string, string) {
648 cert.KeyBits = 512
649- srvCert, srvKey, err := cert.NewServer("testing-env", []byte(CACert), []byte(CAKey), time.Now().AddDate(10, 0, 0))
650+ var hostnames []string
651+ srvCert, srvKey, err := cert.NewServer([]byte(CACert), []byte(CAKey), time.Now().AddDate(10, 0, 0), hostnames)
652 if err != nil {
653 panic(err)
654 }

Subscribers

People subscribed via source and target branches

to status/vote changes: