github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/environs/httpstorage/storage.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package httpstorage
     5  
     6  import (
     7  	"crypto/tls"
     8  	"crypto/x509"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"net/http"
    13  	"net/url"
    14  	"sort"
    15  	"strings"
    16  	"sync"
    17  
    18  	"github.com/juju/errors"
    19  	"github.com/juju/loggo"
    20  	"github.com/juju/utils"
    21  
    22  	"github.com/juju/juju/environs/storage"
    23  )
    24  
    25  var logger = loggo.GetLogger("juju.environs.httpstorage")
    26  
    27  // storage implements the storage.Storage interface.
    28  type localStorage struct {
    29  	addr   string
    30  	client *http.Client
    31  
    32  	authkey           string
    33  	httpsBaseURL      string
    34  	httpsBaseURLError error
    35  	httpsBaseURLOnce  sync.Once
    36  }
    37  
    38  // Client returns a storage object that will talk to the
    39  // storage server at the given network address (see Serve)
    40  func Client(addr string) storage.Storage {
    41  	return &localStorage{
    42  		addr:   addr,
    43  		client: utils.GetValidatingHTTPClient(),
    44  	}
    45  }
    46  
    47  // ClientTLS returns a storage object that will talk to the
    48  // storage server at the given network address (see Serve),
    49  // using TLS. The client is given an authentication key,
    50  // which the server will verify for Put and Remove* operations.
    51  func ClientTLS(addr string, caCertPEM string, authkey string) (storage.Storage, error) {
    52  	logger.Debugf("using https storage at %q", addr)
    53  	caCerts := x509.NewCertPool()
    54  	if !caCerts.AppendCertsFromPEM([]byte(caCertPEM)) {
    55  		return nil, errors.New("error adding CA certificate to pool")
    56  	}
    57  	return &localStorage{
    58  		addr:    addr,
    59  		authkey: authkey,
    60  		client: &http.Client{
    61  			Transport: utils.NewHttpTLSTransport(&tls.Config{RootCAs: caCerts}),
    62  		},
    63  	}, nil
    64  }
    65  
    66  func (s *localStorage) getHTTPSBaseURL() (string, error) {
    67  	url, _ := s.URL("") // never fails
    68  	resp, err := s.client.Head(url)
    69  	if err != nil {
    70  		return "", err
    71  	}
    72  	resp.Body.Close()
    73  	if resp.StatusCode != http.StatusOK {
    74  		return "", fmt.Errorf("Could not access file storage: %v %s", url, resp.Status)
    75  	}
    76  	httpsURL, err := resp.Location()
    77  	if err != nil {
    78  		return "", err
    79  	}
    80  	return httpsURL.String(), nil
    81  }
    82  
    83  // Get opens the given storage file and returns a ReadCloser
    84  // that can be used to read its contents. It is the caller's
    85  // responsibility to close it after use. If the name does not
    86  // exist, it should return a *NotFoundError.
    87  func (s *localStorage) Get(name string) (io.ReadCloser, error) {
    88  	logger.Debugf("getting %q from storage", name)
    89  	url, err := s.URL(name)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  	resp, err := s.client.Get(url)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  	if resp.StatusCode != http.StatusOK {
    98  		return nil, errors.NotFoundf("file %q", name)
    99  	}
   100  	return resp.Body, nil
   101  }
   102  
   103  // List lists all names in the storage with the given prefix, in
   104  // alphabetical order. The names in the storage are considered
   105  // to be in a flat namespace, so the prefix may include slashes
   106  // and the names returned are the full names for the matching
   107  // entries.
   108  func (s *localStorage) List(prefix string) ([]string, error) {
   109  	url, err := s.URL(prefix)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	resp, err := s.client.Get(url + "*")
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  	if resp.StatusCode != http.StatusOK {
   118  		// If the path is not found, it's not an error
   119  		// because it's only created when the first
   120  		// file is put.
   121  		if resp.StatusCode == http.StatusNotFound {
   122  			return []string{}, nil
   123  		}
   124  		return nil, fmt.Errorf("%s", resp.Status)
   125  	}
   126  	defer resp.Body.Close()
   127  	body, err := ioutil.ReadAll(resp.Body)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	if len(body) == 0 {
   132  		return nil, nil
   133  	}
   134  	names := strings.Split(string(body), "\n")
   135  	sort.Strings(names)
   136  	return names, nil
   137  }
   138  
   139  // URL returns a URL that can be used to access the given storage file.
   140  func (s *localStorage) URL(name string) (string, error) {
   141  	return fmt.Sprintf("http://%s/%s", s.addr, name), nil
   142  }
   143  
   144  // modURL returns a URL that can be used to modify the given storage file.
   145  func (s *localStorage) modURL(name string) (string, error) {
   146  	if s.authkey == "" {
   147  		return s.URL(name)
   148  	}
   149  	s.httpsBaseURLOnce.Do(func() {
   150  		s.httpsBaseURL, s.httpsBaseURLError = s.getHTTPSBaseURL()
   151  	})
   152  	if s.httpsBaseURLError != nil {
   153  		return "", s.httpsBaseURLError
   154  	}
   155  	v := url.Values{}
   156  	v.Set("authkey", s.authkey)
   157  	return fmt.Sprintf("%s%s?%s", s.httpsBaseURL, name, v.Encode()), nil
   158  }
   159  
   160  // DefaultConsistencyStrategy is specified in the StorageReader interface.
   161  func (s *localStorage) DefaultConsistencyStrategy() utils.AttemptStrategy {
   162  	return utils.AttemptStrategy{}
   163  }
   164  
   165  // ShouldRetry is specified in the StorageReader interface.
   166  func (s *localStorage) ShouldRetry(err error) bool {
   167  	return false
   168  }
   169  
   170  // Put reads from r and writes to the given storage file.
   171  // The length must be set to the total length of the file.
   172  func (s *localStorage) Put(name string, r io.Reader, length int64) error {
   173  	logger.Debugf("putting %q (len %d) to storage", name, length)
   174  	url, err := s.modURL(name)
   175  	if err != nil {
   176  		return err
   177  	}
   178  
   179  	// Here we wrap up the reader.  For some freaky unexplainable reason, the
   180  	// http library will call Close on the reader if it has a Close method
   181  	// available.  Since we sometimes reuse the reader, especially when
   182  	// putting tools, we don't want Close called.  So we wrap the reader in a
   183  	// struct so the Close method is not exposed.
   184  	justReader := struct{ io.Reader }{r}
   185  	req, err := http.NewRequest("PUT", url, justReader)
   186  	if err != nil {
   187  		return err
   188  	}
   189  	req.Header.Set("Content-Type", "application/octet-stream")
   190  	req.ContentLength = length
   191  	resp, err := s.client.Do(req)
   192  	if err != nil {
   193  		return err
   194  	}
   195  	if resp.StatusCode != 201 {
   196  		return fmt.Errorf("%d %s", resp.StatusCode, resp.Status)
   197  	}
   198  	return nil
   199  }
   200  
   201  // Remove removes the given file from the environment's
   202  // storage. It should not return an error if the file does
   203  // not exist.
   204  func (s *localStorage) Remove(name string) error {
   205  	url, err := s.modURL(name)
   206  	if err != nil {
   207  		return err
   208  	}
   209  	req, err := http.NewRequest("DELETE", url, nil)
   210  	if err != nil {
   211  		return err
   212  	}
   213  	resp, err := s.client.Do(req)
   214  	if err != nil {
   215  		return err
   216  	}
   217  	if resp.StatusCode != http.StatusOK {
   218  		return fmt.Errorf("%d %s", resp.StatusCode, resp.Status)
   219  	}
   220  	return nil
   221  }
   222  
   223  func (s *localStorage) RemoveAll() error {
   224  	return storage.RemoveAll(s)
   225  }