launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/environs/httpstorage/backend_test.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package httpstorage_test 5 6 import ( 7 "bytes" 8 "crypto/tls" 9 "crypto/x509" 10 "fmt" 11 "io/ioutil" 12 "net" 13 "net/http" 14 "os" 15 "path/filepath" 16 "strings" 17 stdtesting "testing" 18 19 gc "launchpad.net/gocheck" 20 21 "launchpad.net/juju-core/environs/filestorage" 22 "launchpad.net/juju-core/environs/httpstorage" 23 coretesting "launchpad.net/juju-core/testing" 24 jc "launchpad.net/juju-core/testing/checkers" 25 "launchpad.net/juju-core/testing/testbase" 26 "launchpad.net/juju-core/utils" 27 ) 28 29 const testAuthkey = "jabberwocky" 30 31 func TestLocal(t *stdtesting.T) { 32 gc.TestingT(t) 33 } 34 35 type backendSuite struct { 36 testbase.LoggingSuite 37 } 38 39 var _ = gc.Suite(&backendSuite{}) 40 41 // startServer starts a new local storage server 42 // using a temporary directory and returns the listener, 43 // a base URL for the server and the directory path. 44 func startServer(c *gc.C) (listener net.Listener, url, dataDir string) { 45 dataDir = c.MkDir() 46 embedded, err := filestorage.NewFileStorageWriter(dataDir, filestorage.UseDefaultTmpDir) 47 c.Assert(err, gc.IsNil) 48 listener, err = httpstorage.Serve("localhost:0", embedded) 49 c.Assert(err, gc.IsNil) 50 return listener, fmt.Sprintf("http://%s/", listener.Addr()), dataDir 51 } 52 53 // startServerTLS starts a new TLS-based local storage server 54 // using a temporary directory and returns the listener, 55 // a base URL for the server and the directory path. 56 func startServerTLS(c *gc.C) (listener net.Listener, url, dataDir string) { 57 dataDir = c.MkDir() 58 embedded, err := filestorage.NewFileStorageWriter(dataDir, filestorage.UseDefaultTmpDir) 59 c.Assert(err, gc.IsNil) 60 hostnames := []string{"127.0.0.1"} 61 caCertPEM := []byte(coretesting.CACert) 62 caKeyPEM := []byte(coretesting.CAKey) 63 listener, err = httpstorage.ServeTLS("127.0.0.1:0", embedded, caCertPEM, caKeyPEM, hostnames, testAuthkey) 64 c.Assert(err, gc.IsNil) 65 return listener, fmt.Sprintf("http://localhost:%d/", listener.Addr().(*net.TCPAddr).Port), dataDir 66 } 67 68 type testCase struct { 69 name string 70 content string 71 found []string 72 status int 73 } 74 75 var getTests = []testCase{ 76 { 77 // Get existing file. 78 name: "foo", 79 content: "this is file 'foo'", 80 }, 81 { 82 // Get existing file. 83 name: "bar", 84 content: "this is file 'bar'", 85 }, 86 { 87 // Get existing file. 88 name: "baz", 89 content: "this is file 'baz'", 90 }, 91 { 92 // Get existing file. 93 name: "yadda", 94 content: "this is file 'yadda'", 95 }, 96 { 97 // Get existing file from nested directory. 98 name: "inner/fooin", 99 content: "this is inner file 'fooin'", 100 }, 101 { 102 // Get existing file from nested directory. 103 name: "inner/barin", 104 content: "this is inner file 'barin'", 105 }, 106 { 107 // Get non-existing file. 108 name: "dummy", 109 status: 404, 110 }, 111 { 112 // Get non-existing file from nested directory. 113 name: "inner/dummy", 114 status: 404, 115 }, 116 { 117 // Get with a relative path ".." based on the 118 // root is passed without invoking the handler 119 // function. 120 name: "../dummy", 121 status: 404, 122 }, 123 { 124 // Get with a relative path ".." based on the 125 // root is passed without invoking the handler 126 // function. 127 name: "../foo", 128 content: "this is file 'foo'", 129 }, 130 { 131 // Get on a directory returns a 404 as it is 132 // not a file. 133 name: "inner", 134 status: 404, 135 }, 136 } 137 138 func (s *backendSuite) TestHeadNonAuth(c *gc.C) { 139 // HEAD is unsupported for non-authenticating servers. 140 listener, url, _ := startServer(c) 141 defer listener.Close() 142 resp, err := http.Head(url) 143 c.Assert(err, gc.IsNil) 144 c.Assert(resp.StatusCode, gc.Equals, http.StatusMethodNotAllowed) 145 } 146 147 func (s *backendSuite) TestHeadAuth(c *gc.C) { 148 // HEAD on an authenticating server will return the HTTPS counterpart URL. 149 client, url, datadir := s.tlsServerAndClient(c) 150 createTestData(c, datadir) 151 152 resp, err := client.Head(url) 153 c.Assert(err, gc.IsNil) 154 c.Assert(resp.StatusCode, gc.Equals, http.StatusOK) 155 location, err := resp.Location() 156 c.Assert(err, gc.IsNil) 157 c.Assert(location.String(), gc.Matches, "https://localhost:[0-9]{5}/") 158 testGet(c, client, location.String()) 159 } 160 161 func (s *backendSuite) TestHeadCustomHost(c *gc.C) { 162 // HEAD with a custom "Host:" header; the server should respond 163 // with a Location with the specified Host header. 164 client, url, _ := s.tlsServerAndClient(c) 165 req, err := http.NewRequest("HEAD", url+"arbitrary", nil) 166 c.Assert(err, gc.IsNil) 167 req.Host = "notarealhost" 168 resp, err := client.Do(req) 169 c.Assert(err, gc.IsNil) 170 c.Assert(resp.StatusCode, gc.Equals, http.StatusOK) 171 location, err := resp.Location() 172 c.Assert(err, gc.IsNil) 173 c.Assert(location.String(), gc.Matches, "https://notarealhost:[0-9]{5}/arbitrary") 174 } 175 176 func (s *backendSuite) TestGet(c *gc.C) { 177 // Test retrieving a file from a storage. 178 listener, url, dataDir := startServer(c) 179 defer listener.Close() 180 createTestData(c, dataDir) 181 testGet(c, http.DefaultClient, url) 182 } 183 184 func testGet(c *gc.C, client *http.Client, url string) { 185 check := func(tc testCase) { 186 resp, err := client.Get(url + tc.name) 187 c.Assert(err, gc.IsNil) 188 if tc.status != 0 { 189 c.Assert(resp.StatusCode, gc.Equals, tc.status) 190 return 191 } else { 192 c.Assert(resp.StatusCode, gc.Equals, http.StatusOK) 193 } 194 defer resp.Body.Close() 195 var buf bytes.Buffer 196 _, err = buf.ReadFrom(resp.Body) 197 c.Assert(err, gc.IsNil) 198 c.Assert(buf.String(), gc.Equals, tc.content) 199 } 200 for _, tc := range getTests { 201 check(tc) 202 } 203 } 204 205 var listTests = []testCase{ 206 { 207 // List with a full filename. 208 name: "foo", 209 found: []string{"foo"}, 210 }, 211 { 212 // List with a name matching two files. 213 name: "ba", 214 found: []string{"bar", "baz"}, 215 }, 216 { 217 // List the contents of a directory. 218 name: "inner/", 219 found: []string{"inner/barin", "inner/bazin", "inner/fooin"}, 220 }, 221 { 222 // List with a name matching two files in 223 // a directory. 224 name: "inner/ba", 225 found: []string{"inner/barin", "inner/bazin"}, 226 }, 227 { 228 // List with no name also lists the contents of all 229 // directories. 230 name: "", 231 found: []string{"bar", "baz", "foo", "inner/barin", "inner/bazin", "inner/fooin", "yadda"}, 232 }, 233 { 234 // List with a non-matching name returns an empty 235 // body which is evaluated to a slice with an empty 236 // string in the test (simplification). 237 name: "zzz", 238 found: []string{""}, 239 }, 240 { 241 // List with a relative path ".." based on the 242 // root is passed without invoking the handler 243 // function. So returns the contents of all 244 // directories. 245 name: "../", 246 found: []string{"bar", "baz", "foo", "inner/barin", "inner/bazin", "inner/fooin", "yadda"}, 247 }, 248 } 249 250 func (s *backendSuite) TestList(c *gc.C) { 251 // Test listing file of a storage. 252 listener, url, dataDir := startServer(c) 253 defer listener.Close() 254 createTestData(c, dataDir) 255 testList(c, http.DefaultClient, url) 256 } 257 258 func testList(c *gc.C, client *http.Client, url string) { 259 check := func(tc testCase) { 260 resp, err := client.Get(url + tc.name + "*") 261 c.Assert(err, gc.IsNil) 262 if tc.status != 0 { 263 c.Assert(resp.StatusCode, gc.Equals, tc.status) 264 return 265 } 266 defer resp.Body.Close() 267 var buf bytes.Buffer 268 _, err = buf.ReadFrom(resp.Body) 269 c.Assert(err, gc.IsNil) 270 names := strings.Split(buf.String(), "\n") 271 c.Assert(names, gc.DeepEquals, tc.found) 272 } 273 for i, tc := range listTests { 274 c.Logf("test %d", i) 275 check(tc) 276 } 277 } 278 279 var putTests = []testCase{ 280 { 281 // Put a file in the root directory. 282 name: "porterhouse", 283 content: "this is the sent file 'porterhouse'", 284 }, 285 { 286 // Put a file with a relative path ".." is resolved 287 // a redirect 301 by the Go HTTP daemon. The handler 288 // isn't aware of it. 289 name: "../no-way", 290 status: 301, 291 }, 292 { 293 // Put a file in a nested directory. 294 name: "deep/cambridge", 295 content: "this is the sent file 'deep/cambridge'", 296 }, 297 } 298 299 func (s *backendSuite) TestPut(c *gc.C) { 300 // Test sending a file to the storage. 301 listener, url, dataDir := startServer(c) 302 defer listener.Close() 303 createTestData(c, dataDir) 304 testPut(c, http.DefaultClient, url, dataDir, true) 305 } 306 307 func testPut(c *gc.C, client *http.Client, url, dataDir string, authorized bool) { 308 check := func(tc testCase) { 309 req, err := http.NewRequest("PUT", url+tc.name, bytes.NewBufferString(tc.content)) 310 c.Assert(err, gc.IsNil) 311 req.Header.Set("Content-Type", "application/octet-stream") 312 resp, err := client.Do(req) 313 c.Assert(err, gc.IsNil) 314 if tc.status != 0 { 315 c.Assert(resp.StatusCode, gc.Equals, tc.status) 316 return 317 } else if !authorized { 318 c.Assert(resp.StatusCode, gc.Equals, http.StatusUnauthorized) 319 return 320 } 321 c.Assert(resp.StatusCode, gc.Equals, http.StatusCreated) 322 323 fp := filepath.Join(dataDir, tc.name) 324 b, err := ioutil.ReadFile(fp) 325 c.Assert(err, gc.IsNil) 326 c.Assert(string(b), gc.Equals, tc.content) 327 } 328 for _, tc := range putTests { 329 check(tc) 330 } 331 } 332 333 var removeTests = []testCase{ 334 { 335 // Delete a file in the root directory. 336 name: "fox", 337 content: "the quick brown fox jumps over the lazy dog", 338 }, 339 { 340 // Delete a file in a nested directory. 341 name: "quick/brown/fox", 342 content: "the quick brown fox jumps over the lazy dog", 343 }, 344 { 345 // Delete a non-existing file leads to no error. 346 name: "dog", 347 }, 348 { 349 // Delete a file with a relative path ".." is resolved 350 // a redirect 301 by the Go HTTP daemon. The handler 351 // doesn't get aware of it. 352 name: "../something", 353 status: 301, 354 }, 355 } 356 357 func (s *backendSuite) TestRemove(c *gc.C) { 358 // Test removing a file in the storage. 359 listener, url, dataDir := startServer(c) 360 defer listener.Close() 361 createTestData(c, dataDir) 362 testRemove(c, http.DefaultClient, url, dataDir, true) 363 } 364 365 func testRemove(c *gc.C, client *http.Client, url, dataDir string, authorized bool) { 366 check := func(tc testCase) { 367 fp := filepath.Join(dataDir, tc.name) 368 dir, _ := filepath.Split(fp) 369 err := os.MkdirAll(dir, 0777) 370 c.Assert(err, gc.IsNil) 371 err = ioutil.WriteFile(fp, []byte(tc.content), 0644) 372 c.Assert(err, gc.IsNil) 373 374 req, err := http.NewRequest("DELETE", url+tc.name, nil) 375 c.Assert(err, gc.IsNil) 376 resp, err := client.Do(req) 377 c.Assert(err, gc.IsNil) 378 if tc.status != 0 { 379 c.Assert(resp.StatusCode, gc.Equals, tc.status) 380 return 381 } else if !authorized { 382 c.Assert(resp.StatusCode, gc.Equals, http.StatusUnauthorized) 383 return 384 } 385 c.Assert(resp.StatusCode, gc.Equals, http.StatusOK) 386 387 _, err = os.Stat(fp) 388 c.Assert(os.IsNotExist(err), gc.Equals, true) 389 } 390 for i, tc := range removeTests { 391 c.Logf("test %d", i) 392 check(tc) 393 } 394 } 395 396 func createTestData(c *gc.C, dataDir string) { 397 writeData := func(dir, name, data string) { 398 fn := filepath.Join(dir, name) 399 c.Logf("writing data to %q", fn) 400 err := ioutil.WriteFile(fn, []byte(data), 0644) 401 c.Assert(err, gc.IsNil) 402 } 403 404 writeData(dataDir, "foo", "this is file 'foo'") 405 writeData(dataDir, "bar", "this is file 'bar'") 406 writeData(dataDir, "baz", "this is file 'baz'") 407 writeData(dataDir, "yadda", "this is file 'yadda'") 408 409 innerDir := filepath.Join(dataDir, "inner") 410 err := os.MkdirAll(innerDir, 0777) 411 c.Assert(err, gc.IsNil) 412 413 writeData(innerDir, "fooin", "this is inner file 'fooin'") 414 writeData(innerDir, "barin", "this is inner file 'barin'") 415 writeData(innerDir, "bazin", "this is inner file 'bazin'") 416 } 417 418 func (b *backendSuite) tlsServerAndClient(c *gc.C) (client *http.Client, url, dataDir string) { 419 listener, url, dataDir := startServerTLS(c) 420 b.AddCleanup(func(*gc.C) { listener.Close() }) 421 caCerts := x509.NewCertPool() 422 c.Assert(caCerts.AppendCertsFromPEM([]byte(coretesting.CACert)), jc.IsTrue) 423 client = &http.Client{ 424 Transport: utils.NewHttpTLSTransport(&tls.Config{RootCAs: caCerts}), 425 } 426 return client, url, dataDir 427 } 428 429 func (b *backendSuite) TestTLSUnauthenticatedGet(c *gc.C) { 430 client, url, dataDir := b.tlsServerAndClient(c) 431 createTestData(c, dataDir) 432 testGet(c, client, url) 433 } 434 435 func (b *backendSuite) TestTLSUnauthenticatedList(c *gc.C) { 436 client, url, dataDir := b.tlsServerAndClient(c) 437 createTestData(c, dataDir) 438 testList(c, client, url) 439 } 440 441 func (b *backendSuite) TestTLSUnauthenticatedPut(c *gc.C) { 442 client, url, dataDir := b.tlsServerAndClient(c) 443 createTestData(c, dataDir) 444 testPut(c, client, url, dataDir, false) 445 } 446 447 func (b *backendSuite) TestTLSUnauthenticatedRemove(c *gc.C) { 448 client, url, dataDir := b.tlsServerAndClient(c) 449 createTestData(c, dataDir) 450 testRemove(c, client, url, dataDir, false) 451 }