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