github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/state/apiserver/charms.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package apiserver 5 6 import ( 7 "archive/zip" 8 "crypto/sha256" 9 "encoding/hex" 10 "encoding/json" 11 "fmt" 12 "io" 13 "io/ioutil" 14 "mime" 15 "net/http" 16 "net/url" 17 "os" 18 "path" 19 "path/filepath" 20 "sort" 21 "strconv" 22 "strings" 23 24 "github.com/juju/errors" 25 ziputil "github.com/juju/utils/zip" 26 27 "github.com/juju/juju/charm" 28 "github.com/juju/juju/environs" 29 "github.com/juju/juju/state/api/params" 30 ) 31 32 // charmsHandler handles charm upload through HTTPS in the API server. 33 type charmsHandler struct { 34 httpHandler 35 dataDir string 36 } 37 38 // bundleContentSenderFunc functions are responsible for sending a 39 // response related to a charm bundle. 40 type bundleContentSenderFunc func(w http.ResponseWriter, r *http.Request, bundle *charm.Bundle) 41 42 func (h *charmsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 43 if err := h.authenticate(r); err != nil { 44 h.authError(w, h) 45 return 46 } 47 if err := h.validateEnvironUUID(r); err != nil { 48 h.sendError(w, http.StatusNotFound, err.Error()) 49 return 50 } 51 52 switch r.Method { 53 case "POST": 54 // Add a local charm to the store provider. 55 // Requires a "series" query specifying the series to use for the charm. 56 charmURL, err := h.processPost(r) 57 if err != nil { 58 h.sendError(w, http.StatusBadRequest, err.Error()) 59 return 60 } 61 h.sendJSON(w, http.StatusOK, ¶ms.CharmsResponse{CharmURL: charmURL.String()}) 62 case "GET": 63 // Retrieve or list charm files. 64 // Requires "url" (charm URL) and an optional "file" (the path to the 65 // charm file) to be included in the query. 66 if charmArchivePath, filePath, err := h.processGet(r); err != nil { 67 // An error occurred retrieving the charm bundle. 68 h.sendError(w, http.StatusBadRequest, err.Error()) 69 } else if filePath == "" { 70 // The client requested the list of charm files. 71 sendBundleContent(w, r, charmArchivePath, h.manifestSender) 72 } else { 73 // The client requested a specific file. 74 sendBundleContent(w, r, charmArchivePath, h.fileSender(filePath)) 75 } 76 default: 77 h.sendError(w, http.StatusMethodNotAllowed, fmt.Sprintf("unsupported method: %q", r.Method)) 78 } 79 } 80 81 // sendJSON sends a JSON-encoded response to the client. 82 func (h *charmsHandler) sendJSON(w http.ResponseWriter, statusCode int, response *params.CharmsResponse) error { 83 w.Header().Set("Content-Type", "application/json") 84 w.WriteHeader(statusCode) 85 body, err := json.Marshal(response) 86 if err != nil { 87 return err 88 } 89 w.Write(body) 90 return nil 91 } 92 93 // sendBundleContent uses the given bundleContentSenderFunc to send a response 94 // related to the charm archive located in the given archivePath. 95 func sendBundleContent(w http.ResponseWriter, r *http.Request, archivePath string, sender bundleContentSenderFunc) { 96 bundle, err := charm.ReadBundle(archivePath) 97 if err != nil { 98 http.Error( 99 w, fmt.Sprintf("unable to read archive in %q: %v", archivePath, err), 100 http.StatusInternalServerError) 101 return 102 } 103 // The bundleContentSenderFunc will set up and send an appropriate response. 104 sender(w, r, bundle) 105 } 106 107 // manifestSender sends a JSON-encoded response to the client including the 108 // list of files contained in the charm bundle. 109 func (h *charmsHandler) manifestSender(w http.ResponseWriter, r *http.Request, bundle *charm.Bundle) { 110 manifest, err := bundle.Manifest() 111 if err != nil { 112 http.Error( 113 w, fmt.Sprintf("unable to read archive in %q: %v", bundle.Path, err), 114 http.StatusInternalServerError) 115 return 116 } 117 h.sendJSON(w, http.StatusOK, ¶ms.CharmsResponse{Files: manifest.SortedValues()}) 118 } 119 120 // fileSender returns a bundleContentSenderFunc which is responsible for sending 121 // the contents of filePath included in the given charm bundle. If filePath does 122 // not identify a file or a symlink, a 403 forbidden error is returned. 123 func (h *charmsHandler) fileSender(filePath string) bundleContentSenderFunc { 124 return func(w http.ResponseWriter, r *http.Request, bundle *charm.Bundle) { 125 // TODO(fwereade) 2014-01-27 bug #1285685 126 // This doesn't handle symlinks helpfully, and should be talking in 127 // terms of bundles rather than zip readers; but this demands thought 128 // and design and is not amenable to a quick fix. 129 zipReader, err := zip.OpenReader(bundle.Path) 130 if err != nil { 131 http.Error( 132 w, fmt.Sprintf("unable to read charm: %v", err), 133 http.StatusInternalServerError) 134 return 135 } 136 defer zipReader.Close() 137 for _, file := range zipReader.File { 138 if path.Clean(file.Name) != filePath { 139 continue 140 } 141 fileInfo := file.FileInfo() 142 if fileInfo.IsDir() { 143 http.Error(w, "directory listing not allowed", http.StatusForbidden) 144 return 145 } 146 contents, err := file.Open() 147 if err != nil { 148 http.Error( 149 w, fmt.Sprintf("unable to read file %q: %v", filePath, err), 150 http.StatusInternalServerError) 151 return 152 } 153 defer contents.Close() 154 ctype := mime.TypeByExtension(filepath.Ext(filePath)) 155 if ctype != "" { 156 w.Header().Set("Content-Type", ctype) 157 } 158 w.Header().Set("Content-Length", strconv.FormatInt(fileInfo.Size(), 10)) 159 w.WriteHeader(http.StatusOK) 160 io.Copy(w, contents) 161 return 162 } 163 http.NotFound(w, r) 164 return 165 } 166 } 167 168 // sendError sends a JSON-encoded error response. 169 func (h *charmsHandler) sendError(w http.ResponseWriter, statusCode int, message string) error { 170 return h.sendJSON(w, statusCode, ¶ms.CharmsResponse{Error: message}) 171 } 172 173 // processPost handles a charm upload POST request after authentication. 174 func (h *charmsHandler) processPost(r *http.Request) (*charm.URL, error) { 175 query := r.URL.Query() 176 series := query.Get("series") 177 if series == "" { 178 return nil, fmt.Errorf("expected series=URL argument") 179 } 180 // Make sure the content type is zip. 181 contentType := r.Header.Get("Content-Type") 182 if contentType != "application/zip" { 183 return nil, fmt.Errorf("expected Content-Type: application/zip, got: %v", contentType) 184 } 185 tempFile, err := ioutil.TempFile("", "charm") 186 if err != nil { 187 return nil, fmt.Errorf("cannot create temp file: %v", err) 188 } 189 defer tempFile.Close() 190 defer os.Remove(tempFile.Name()) 191 if _, err := io.Copy(tempFile, r.Body); err != nil { 192 return nil, fmt.Errorf("error processing file upload: %v", err) 193 } 194 err = h.processUploadedArchive(tempFile.Name()) 195 if err != nil { 196 return nil, err 197 } 198 archive, err := charm.ReadBundle(tempFile.Name()) 199 if err != nil { 200 return nil, fmt.Errorf("invalid charm archive: %v", err) 201 } 202 // We got it, now let's reserve a charm URL for it in state. 203 archiveURL := &charm.URL{ 204 Reference: charm.Reference{ 205 Schema: "local", 206 Name: archive.Meta().Name, 207 Revision: archive.Revision(), 208 }, 209 Series: series, 210 } 211 preparedURL, err := h.state.PrepareLocalCharmUpload(archiveURL) 212 if err != nil { 213 return nil, err 214 } 215 // Now we need to repackage it with the reserved URL, upload it to 216 // provider storage and update the state. 217 err = h.repackageAndUploadCharm(archive, preparedURL) 218 if err != nil { 219 return nil, err 220 } 221 // All done. 222 return preparedURL, nil 223 } 224 225 // processUploadedArchive opens the given charm archive from path, 226 // inspects it to see if it has all files at the root of the archive 227 // or it has subdirs. It repackages the archive so it has all the 228 // files at the root dir, if necessary, replacing the original archive 229 // at path. 230 func (h *charmsHandler) processUploadedArchive(path string) error { 231 // Open the archive as a zip. 232 f, err := os.OpenFile(path, os.O_RDWR, 0644) 233 if err != nil { 234 return err 235 } 236 defer f.Close() 237 fi, err := f.Stat() 238 if err != nil { 239 return err 240 } 241 zipr, err := zip.NewReader(f, fi.Size()) 242 if err != nil { 243 return errors.Annotate(err, "cannot open charm archive") 244 } 245 246 // Find out the root dir prefix from the archive. 247 rootDir, err := h.findArchiveRootDir(zipr) 248 if err != nil { 249 return errors.Annotate(err, "cannot read charm archive") 250 } 251 if rootDir == "." { 252 // Normal charm, just use charm.ReadBundle(). 253 return nil 254 } 255 256 // There is one or more subdirs, so we need extract it to a temp 257 // dir and then read it as a charm dir. 258 tempDir, err := ioutil.TempDir("", "charm-extract") 259 if err != nil { 260 return errors.Annotate(err, "cannot create temp directory") 261 } 262 defer os.RemoveAll(tempDir) 263 if err := ziputil.Extract(zipr, tempDir, rootDir); err != nil { 264 return errors.Annotate(err, "cannot extract charm archive") 265 } 266 dir, err := charm.ReadDir(tempDir) 267 if err != nil { 268 return errors.Annotate(err, "cannot read extracted archive") 269 } 270 271 // Now repackage the dir as a bundle at the original path. 272 if err := f.Truncate(0); err != nil { 273 return err 274 } 275 if err := dir.BundleTo(f); err != nil { 276 return err 277 } 278 return nil 279 } 280 281 // findArchiveRootDir scans a zip archive and returns the rootDir of 282 // the archive, the one containing metadata.yaml, config.yaml and 283 // revision files, or an error if the archive appears invalid. 284 func (h *charmsHandler) findArchiveRootDir(zipr *zip.Reader) (string, error) { 285 paths, err := ziputil.Find(zipr, "metadata.yaml") 286 if err != nil { 287 return "", err 288 } 289 switch len(paths) { 290 case 0: 291 return "", fmt.Errorf("invalid charm archive: missing metadata.yaml") 292 case 1: 293 default: 294 sort.Sort(byDepth(paths)) 295 if depth(paths[0]) == depth(paths[1]) { 296 return "", fmt.Errorf("invalid charm archive: ambiguous root directory") 297 } 298 } 299 return filepath.Dir(paths[0]), nil 300 } 301 302 func depth(path string) int { 303 return strings.Count(path, "/") 304 } 305 306 type byDepth []string 307 308 func (d byDepth) Len() int { return len(d) } 309 func (d byDepth) Swap(i, j int) { d[i], d[j] = d[j], d[i] } 310 func (d byDepth) Less(i, j int) bool { return depth(d[i]) < depth(d[j]) } 311 312 // repackageAndUploadCharm expands the given charm archive to a 313 // temporary directoy, repackages it with the given curl's revision, 314 // then uploads it to providr storage, and finally updates the state. 315 func (h *charmsHandler) repackageAndUploadCharm(archive *charm.Bundle, curl *charm.URL) error { 316 // Create a temp dir to contain the extracted charm 317 // dir and the repackaged archive. 318 tempDir, err := ioutil.TempDir("", "charm-download") 319 if err != nil { 320 return errors.Annotate(err, "cannot create temp directory") 321 } 322 defer os.RemoveAll(tempDir) 323 extractPath := filepath.Join(tempDir, "extracted") 324 repackagedPath := filepath.Join(tempDir, "repackaged.zip") 325 repackagedArchive, err := os.Create(repackagedPath) 326 if err != nil { 327 return errors.Annotate(err, "cannot repackage uploaded charm") 328 } 329 defer repackagedArchive.Close() 330 331 // Expand and repack it with the revision specified by curl. 332 archive.SetRevision(curl.Revision) 333 if err := archive.ExpandTo(extractPath); err != nil { 334 return errors.Annotate(err, "cannot extract uploaded charm") 335 } 336 charmDir, err := charm.ReadDir(extractPath) 337 if err != nil { 338 return errors.Annotate(err, "cannot read extracted charm") 339 } 340 341 // Bundle the charm and calculate its sha256 hash at the 342 // same time. 343 hash := sha256.New() 344 err = charmDir.BundleTo(io.MultiWriter(hash, repackagedArchive)) 345 if err != nil { 346 return errors.Annotate(err, "cannot repackage uploaded charm") 347 } 348 bundleSHA256 := hex.EncodeToString(hash.Sum(nil)) 349 size, err := repackagedArchive.Seek(0, 2) 350 if err != nil { 351 return errors.Annotate(err, "cannot get charm file size") 352 } 353 354 // Now upload to provider storage. 355 if _, err := repackagedArchive.Seek(0, 0); err != nil { 356 return errors.Annotate(err, "cannot rewind the charm file reader") 357 } 358 storage, err := environs.GetStorage(h.state) 359 if err != nil { 360 return errors.Annotate(err, "cannot access provider storage") 361 } 362 name := charm.Quote(curl.String()) 363 if err := storage.Put(name, repackagedArchive, size); err != nil { 364 return errors.Annotate(err, "cannot upload charm to provider storage") 365 } 366 storageURL, err := storage.URL(name) 367 if err != nil { 368 return errors.Annotate(err, "cannot get storage URL for charm") 369 } 370 bundleURL, err := url.Parse(storageURL) 371 if err != nil { 372 return errors.Annotate(err, "cannot parse storage URL") 373 } 374 375 // And finally, update state. 376 _, err = h.state.UpdateUploadedCharm(archive, curl, bundleURL, bundleSHA256) 377 if err != nil { 378 return errors.Annotate(err, "cannot update uploaded charm in state") 379 } 380 return nil 381 } 382 383 // processGet handles a charm file GET request after authentication. 384 // It returns the bundle path, the requested file path (if any) and an error. 385 func (h *charmsHandler) processGet(r *http.Request) (string, string, error) { 386 query := r.URL.Query() 387 388 // Retrieve and validate query parameters. 389 curl := query.Get("url") 390 if curl == "" { 391 return "", "", fmt.Errorf("expected url=CharmURL query argument") 392 } 393 var filePath string 394 file := query.Get("file") 395 if file == "" { 396 filePath = "" 397 } else { 398 filePath = path.Clean(file) 399 } 400 401 // Prepare the bundle directories. 402 name := charm.Quote(curl) 403 charmArchivePath := filepath.Join(h.dataDir, "charm-get-cache", name+".zip") 404 405 // Check if the charm archive is already in the cache. 406 if _, err := os.Stat(charmArchivePath); os.IsNotExist(err) { 407 // Download the charm archive and save it to the cache. 408 if err = h.downloadCharm(name, charmArchivePath); err != nil { 409 return "", "", fmt.Errorf("unable to retrieve and save the charm: %v", err) 410 } 411 } else if err != nil { 412 return "", "", fmt.Errorf("cannot access the charms cache: %v", err) 413 } 414 return charmArchivePath, filePath, nil 415 } 416 417 // downloadCharm downloads the given charm name from the provider storage and 418 // saves the corresponding zip archive to the given charmArchivePath. 419 func (h *charmsHandler) downloadCharm(name, charmArchivePath string) error { 420 // Get the provider storage. 421 storage, err := environs.GetStorage(h.state) 422 if err != nil { 423 return errors.Annotate(err, "cannot access provider storage") 424 } 425 426 // Use the storage to retrieve and save the charm archive. 427 reader, err := storage.Get(name) 428 if err != nil { 429 return errors.Annotate(err, "charm not found in the provider storage") 430 } 431 defer reader.Close() 432 data, err := ioutil.ReadAll(reader) 433 if err != nil { 434 return errors.Annotate(err, "cannot read charm data") 435 } 436 // In order to avoid races, the archive is saved in a temporary file which 437 // is then atomically renamed. The temporary file is created in the 438 // charm cache directory so that we can safely assume the rename source and 439 // target live in the same file system. 440 cacheDir := filepath.Dir(charmArchivePath) 441 if err = os.MkdirAll(cacheDir, 0755); err != nil { 442 return errors.Annotate(err, "cannot create the charms cache") 443 } 444 tempCharmArchive, err := ioutil.TempFile(cacheDir, "charm") 445 if err != nil { 446 return errors.Annotate(err, "cannot create charm archive temp file") 447 } 448 defer tempCharmArchive.Close() 449 if err = ioutil.WriteFile(tempCharmArchive.Name(), data, 0644); err != nil { 450 return errors.Annotate(err, "error processing charm archive download") 451 } 452 if err = os.Rename(tempCharmArchive.Name(), charmArchivePath); err != nil { 453 defer os.Remove(tempCharmArchive.Name()) 454 return errors.Annotate(err, "error renaming the charm archive") 455 } 456 return nil 457 }