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  }