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  }