github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/environs/httpstorage/backend.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 "errors" 10 "fmt" 11 "io/ioutil" 12 "net" 13 "net/http" 14 "strings" 15 "time" 16 17 "github.com/juju/juju/cert" 18 "github.com/juju/juju/environs/storage" 19 ) 20 21 // storageBackend provides HTTP access to a storage object. 22 type storageBackend struct { 23 backend storage.Storage 24 25 // httpsPort is the port to send to clients 26 // if they perform a HEAD request. 27 httpsPort int 28 29 // authkey is non-empty if modifying requests 30 // require an auth key. 31 authkey string 32 } 33 34 // ServeHTTP handles the HTTP requests to the container. 35 func (s *storageBackend) ServeHTTP(w http.ResponseWriter, req *http.Request) { 36 switch req.Method { 37 case "PUT", "DELETE": 38 // Don't allow modifying operations if there's an HTTPS backend 39 // to handle that, and ensure the user is authorized/authenticated. 40 if s.httpsPort != 0 || !s.authorized(req) { 41 http.Error(w, "unauthorized access", http.StatusUnauthorized) 42 return 43 } 44 } 45 switch req.Method { 46 case "GET": 47 if strings.HasSuffix(req.URL.Path, "*") { 48 s.handleList(w, req) 49 } else { 50 s.handleGet(w, req) 51 } 52 case "HEAD": 53 s.handleHead(w, req) 54 case "PUT": 55 s.handlePut(w, req) 56 case "DELETE": 57 s.handleDelete(w, req) 58 default: 59 http.Error(w, "method "+req.Method+" is not supported", http.StatusMethodNotAllowed) 60 } 61 } 62 63 // authorized checks that either the storage does not require 64 // authorization, or the user has specified the correct auth key. 65 func (s *storageBackend) authorized(req *http.Request) bool { 66 if s.authkey == "" { 67 return true 68 } 69 return req.URL.Query().Get("authkey") == s.authkey 70 } 71 72 // hostOnly splits a host of the form host, or host:port, 73 // into its host and port parts, and returns the host part. 74 func hostOnly(host string) (string, error) { 75 hostonly, _, err := net.SplitHostPort(host) 76 if err != nil { 77 // err may be because of missing :port. Checking 78 // the error message is brittle, so let's try 79 // again with ":0" tacked on the end. 80 var err2 error 81 hostonly, _, err = net.SplitHostPort(host + ":0") 82 if err2 != nil { 83 // something heinous, return the original error 84 return "", err 85 } 86 } 87 return hostonly, nil 88 } 89 90 // handleHead returns the HTTPS URL for the specified 91 // path in the Location header. 92 func (s *storageBackend) handleHead(w http.ResponseWriter, req *http.Request) { 93 if s.httpsPort != 0 { 94 host, err := hostOnly(req.Host) 95 if err != nil { 96 http.Error(w, fmt.Sprintf("failed to split host: %v", err), http.StatusBadRequest) 97 return 98 } 99 url := fmt.Sprintf("https://%s:%d%s", host, s.httpsPort, req.URL.Path) 100 w.Header().Set("Location", url) 101 } else { 102 http.Error(w, "method HEAD is not supported", http.StatusMethodNotAllowed) 103 return 104 } 105 w.WriteHeader(http.StatusOK) 106 } 107 108 // handleGet returns a storage file to the client. 109 func (s *storageBackend) handleGet(w http.ResponseWriter, req *http.Request) { 110 readcloser, err := s.backend.Get(req.URL.Path[1:]) 111 if err != nil { 112 http.Error(w, fmt.Sprint(err), http.StatusNotFound) 113 return 114 } 115 defer readcloser.Close() 116 data, err := ioutil.ReadAll(readcloser) 117 if err != nil { 118 http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) 119 return 120 } 121 w.Header().Set("Content-Type", "application/octet-stream") 122 w.Write(data) 123 } 124 125 // handleList returns the file names in the storage to the client. 126 func (s *storageBackend) handleList(w http.ResponseWriter, req *http.Request) { 127 prefix := req.URL.Path 128 prefix = prefix[1 : len(prefix)-1] // drop the leading '/' and trailing '*' 129 names, err := s.backend.List(prefix) 130 if err != nil { 131 http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) 132 return 133 } 134 data := []byte(strings.Join(names, "\n")) 135 w.Header().Set("Content-Type", "application/octet-stream") 136 w.Write(data) 137 } 138 139 // handlePut stores data from the client in the storage. 140 func (s *storageBackend) handlePut(w http.ResponseWriter, req *http.Request) { 141 if req.ContentLength < 0 { 142 http.Error(w, "missing or invalid Content-Length header", http.StatusInternalServerError) 143 return 144 } 145 err := s.backend.Put(req.URL.Path[1:], req.Body, req.ContentLength) 146 if err != nil { 147 http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) 148 return 149 } 150 w.WriteHeader(http.StatusCreated) 151 } 152 153 // handleDelete removes a file from the storage. 154 func (s *storageBackend) handleDelete(w http.ResponseWriter, req *http.Request) { 155 if !s.authorized(req) { 156 http.Error(w, "unauthorized access", http.StatusUnauthorized) 157 return 158 } 159 err := s.backend.Remove(req.URL.Path[1:]) 160 if err != nil { 161 http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) 162 return 163 } 164 w.WriteHeader(http.StatusOK) 165 } 166 167 // Serve runs a storage server on the given network address, relaying 168 // requests to the given storage implementation. It returns the network 169 // listener. This can then be attached to with Client. 170 func Serve(addr string, stor storage.Storage) (net.Listener, error) { 171 return serve(addr, stor, nil, "") 172 } 173 174 // ServeTLS runs a storage server on the given network address, relaying 175 // requests to the given storage implementation. The server runs a TLS 176 // listener, and verifies client certificates (if given) against the 177 // specified CA certificate. A client certificate is only required for 178 // PUT and DELETE methods. 179 // 180 // This method returns the network listener, which can then be attached 181 // to with ClientTLS. 182 func ServeTLS(addr string, stor storage.Storage, caCertPEM, caKeyPEM string, hostnames []string, authkey string) (net.Listener, error) { 183 expiry := time.Now().UTC().AddDate(10, 0, 0) 184 certPEM, keyPEM, err := cert.NewServer(caCertPEM, caKeyPEM, expiry, hostnames) 185 if err != nil { 186 return nil, err 187 } 188 serverCert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM)) 189 if err != nil { 190 return nil, err 191 } 192 caCerts := x509.NewCertPool() 193 if !caCerts.AppendCertsFromPEM([]byte(caCertPEM)) { 194 return nil, errors.New("error adding CA certificate to pool") 195 } 196 config := &tls.Config{ 197 NextProtos: []string{"http/1.1"}, 198 Certificates: []tls.Certificate{serverCert}, 199 ClientAuth: tls.VerifyClientCertIfGiven, 200 ClientCAs: caCerts, 201 } 202 return serve(addr, stor, config, authkey) 203 } 204 205 func serve(addr string, stor storage.Storage, tlsConfig *tls.Config, authkey string) (net.Listener, error) { 206 listener, err := net.Listen("tcp", addr) 207 if err != nil { 208 return nil, fmt.Errorf("cannot start listener: %v", err) 209 } 210 backend := &storageBackend{backend: stor} 211 if tlsConfig != nil { 212 tlsBackend := &storageBackend{backend: stor, authkey: authkey} 213 tcpAddr := listener.Addr().(*net.TCPAddr) 214 tlsListener, err := tls.Listen("tcp", fmt.Sprintf("[%s]:0", tcpAddr.IP), tlsConfig) 215 if err != nil { 216 listener.Close() 217 return nil, fmt.Errorf("cannot start TLS listener: %v", err) 218 } 219 backend.httpsPort = tlsListener.Addr().(*net.TCPAddr).Port 220 goServe(tlsListener, tlsBackend) 221 } 222 goServe(listener, backend) 223 return listener, nil 224 } 225 226 func goServe(listener net.Listener, backend *storageBackend) { 227 // Construct a NewServeMux to sanitise request paths. 228 mux := http.NewServeMux() 229 mux.Handle("/", backend) 230 go http.Serve(listener, mux) 231 }