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