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