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 }