github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/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 "launchpad.net/gocheck" 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, gc.IsNil) 47 listener, err = httpstorage.Serve("localhost:0", embedded) 48 c.Assert(err, gc.IsNil) 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, gc.IsNil) 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, gc.IsNil) 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, gc.IsNil) 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, gc.IsNil) 158 c.Assert(resp.StatusCode, gc.Equals, http.StatusOK) 159 location, err := resp.Location() 160 c.Assert(err, gc.IsNil) 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, gc.IsNil) 171 req.Host = "notarealhost" 172 resp, err := client.Do(req) 173 c.Assert(err, gc.IsNil) 174 c.Assert(resp.StatusCode, gc.Equals, http.StatusOK) 175 location, err := resp.Location() 176 c.Assert(err, gc.IsNil) 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, gc.IsNil) 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, gc.IsNil) 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, gc.IsNil) 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, gc.IsNil) 274 names := strings.Split(buf.String(), "\n") 275 c.Assert(names, gc.DeepEquals, tc.found) 276 } 277 for i, tc := range listTests { 278 c.Logf("test %d", i) 279 check(tc) 280 } 281 } 282 283 var putTests = []testCase{ 284 { 285 // Put a file in the root directory. 286 name: "porterhouse", 287 content: "this is the sent file 'porterhouse'", 288 }, 289 { 290 // Put a file with a relative path ".." is resolved 291 // a redirect 301 by the Go HTTP daemon. The handler 292 // isn't aware of it. 293 name: "../no-way", 294 status: 301, 295 }, 296 { 297 // Put a file in a nested directory. 298 name: "deep/cambridge", 299 content: "this is the sent file 'deep/cambridge'", 300 }, 301 } 302 303 func (s *backendSuite) TestPut(c *gc.C) { 304 // Test sending a file to the storage. 305 listener, url, dataDir := startServer(c) 306 defer listener.Close() 307 createTestData(c, dataDir) 308 testPut(c, http.DefaultClient, url, dataDir, true) 309 } 310 311 func testPut(c *gc.C, client *http.Client, url, dataDir string, authorized bool) { 312 check := func(tc testCase) { 313 req, err := http.NewRequest("PUT", url+tc.name, bytes.NewBufferString(tc.content)) 314 c.Assert(err, gc.IsNil) 315 req.Header.Set("Content-Type", "application/octet-stream") 316 resp, err := client.Do(req) 317 c.Assert(err, gc.IsNil) 318 if tc.status != 0 { 319 c.Assert(resp.StatusCode, gc.Equals, tc.status) 320 return 321 } else if !authorized { 322 c.Assert(resp.StatusCode, gc.Equals, http.StatusUnauthorized) 323 return 324 } 325 c.Assert(resp.StatusCode, gc.Equals, http.StatusCreated) 326 327 fp := filepath.Join(dataDir, tc.name) 328 b, err := ioutil.ReadFile(fp) 329 c.Assert(err, gc.IsNil) 330 c.Assert(string(b), gc.Equals, tc.content) 331 } 332 for _, tc := range putTests { 333 check(tc) 334 } 335 } 336 337 var removeTests = []testCase{ 338 { 339 // Delete a file in the root directory. 340 name: "fox", 341 content: "the quick brown fox jumps over the lazy dog", 342 }, 343 { 344 // Delete a file in a nested directory. 345 name: "quick/brown/fox", 346 content: "the quick brown fox jumps over the lazy dog", 347 }, 348 { 349 // Delete a non-existing file leads to no error. 350 name: "dog", 351 }, 352 { 353 // Delete a file with a relative path ".." is resolved 354 // a redirect 301 by the Go HTTP daemon. The handler 355 // doesn't get aware of it. 356 name: "../something", 357 status: 301, 358 }, 359 } 360 361 func (s *backendSuite) TestRemove(c *gc.C) { 362 // Test removing a file in the storage. 363 listener, url, dataDir := startServer(c) 364 defer listener.Close() 365 createTestData(c, dataDir) 366 testRemove(c, http.DefaultClient, url, dataDir, true) 367 } 368 369 func testRemove(c *gc.C, client *http.Client, url, dataDir string, authorized bool) { 370 check := func(tc testCase) { 371 fp := filepath.Join(dataDir, tc.name) 372 dir, _ := filepath.Split(fp) 373 err := os.MkdirAll(dir, 0777) 374 c.Assert(err, gc.IsNil) 375 err = ioutil.WriteFile(fp, []byte(tc.content), 0644) 376 c.Assert(err, gc.IsNil) 377 378 req, err := http.NewRequest("DELETE", url+tc.name, nil) 379 c.Assert(err, gc.IsNil) 380 resp, err := client.Do(req) 381 c.Assert(err, gc.IsNil) 382 if tc.status != 0 { 383 c.Assert(resp.StatusCode, gc.Equals, tc.status) 384 return 385 } else if !authorized { 386 c.Assert(resp.StatusCode, gc.Equals, http.StatusUnauthorized) 387 return 388 } 389 c.Assert(resp.StatusCode, gc.Equals, http.StatusOK) 390 391 _, err = os.Stat(fp) 392 c.Assert(os.IsNotExist(err), gc.Equals, true) 393 } 394 for i, tc := range removeTests { 395 c.Logf("test %d", i) 396 check(tc) 397 } 398 } 399 400 func createTestData(c *gc.C, dataDir string) { 401 writeData := func(dir, name, data string) { 402 fn := filepath.Join(dir, name) 403 c.Logf("writing data to %q", fn) 404 err := ioutil.WriteFile(fn, []byte(data), 0644) 405 c.Assert(err, gc.IsNil) 406 } 407 408 writeData(dataDir, "foo", "this is file 'foo'") 409 writeData(dataDir, "bar", "this is file 'bar'") 410 writeData(dataDir, "baz", "this is file 'baz'") 411 writeData(dataDir, "yadda", "this is file 'yadda'") 412 413 innerDir := filepath.Join(dataDir, "inner") 414 err := os.MkdirAll(innerDir, 0777) 415 c.Assert(err, gc.IsNil) 416 417 writeData(innerDir, "fooin", "this is inner file 'fooin'") 418 writeData(innerDir, "barin", "this is inner file 'barin'") 419 writeData(innerDir, "bazin", "this is inner file 'bazin'") 420 } 421 422 func (b *backendSuite) tlsServerAndClient(c *gc.C) (client *http.Client, url, dataDir string) { 423 listener, url, dataDir := startServerTLS(c) 424 b.AddCleanup(func(*gc.C) { listener.Close() }) 425 caCerts := x509.NewCertPool() 426 c.Assert(caCerts.AppendCertsFromPEM([]byte(coretesting.CACert)), jc.IsTrue) 427 client = &http.Client{ 428 Transport: utils.NewHttpTLSTransport(&tls.Config{RootCAs: caCerts}), 429 } 430 return client, url, dataDir 431 } 432 433 func (b *backendSuite) TestTLSUnauthenticatedGet(c *gc.C) { 434 client, url, dataDir := b.tlsServerAndClient(c) 435 createTestData(c, dataDir) 436 testGet(c, client, url) 437 } 438 439 func (b *backendSuite) TestTLSUnauthenticatedList(c *gc.C) { 440 client, url, dataDir := b.tlsServerAndClient(c) 441 createTestData(c, dataDir) 442 testList(c, client, url) 443 } 444 445 func (b *backendSuite) TestTLSUnauthenticatedPut(c *gc.C) { 446 client, url, dataDir := b.tlsServerAndClient(c) 447 createTestData(c, dataDir) 448 testPut(c, client, url, dataDir, false) 449 } 450 451 func (b *backendSuite) TestTLSUnauthenticatedRemove(c *gc.C) { 452 client, url, dataDir := b.tlsServerAndClient(c) 453 createTestData(c, dataDir) 454 testRemove(c, client, url, dataDir, false) 455 }