github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/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/base64" 10 "encoding/hex" 11 "encoding/json" 12 "fmt" 13 "io" 14 "io/ioutil" 15 "mime" 16 "net/http" 17 "net/url" 18 "os" 19 "path" 20 "path/filepath" 21 "sort" 22 "strconv" 23 "strings" 24 25 "github.com/errgo/errgo" 26 27 "launchpad.net/juju-core/charm" 28 envtesting "launchpad.net/juju-core/environs/testing" 29 "launchpad.net/juju-core/names" 30 "launchpad.net/juju-core/state" 31 "launchpad.net/juju-core/state/api/params" 32 "launchpad.net/juju-core/state/apiserver/common" 33 ziputil "launchpad.net/juju-core/utils/zip" 34 ) 35 36 // charmsHandler handles charm upload through HTTPS in the API server. 37 type charmsHandler struct { 38 state *state.State 39 dataDir string 40 } 41 42 // bundleContentSenderFunc functions are responsible for sending a 43 // response related to a charm bundle. 44 type bundleContentSenderFunc func(w http.ResponseWriter, r *http.Request, bundle *charm.Bundle) 45 46 func (h *charmsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 47 if err := h.authenticate(r); err != nil { 48 h.authError(w) 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 // authenticate parses HTTP basic authentication and authorizes the 174 // request by looking up the provided tag and password against state. 175 func (h *charmsHandler) authenticate(r *http.Request) error { 176 parts := strings.Fields(r.Header.Get("Authorization")) 177 if len(parts) != 2 || parts[0] != "Basic" { 178 // Invalid header format or no header provided. 179 return fmt.Errorf("invalid request format") 180 } 181 // Challenge is a base64-encoded "tag:pass" string. 182 // See RFC 2617, Section 2. 183 challenge, err := base64.StdEncoding.DecodeString(parts[1]) 184 if err != nil { 185 return fmt.Errorf("invalid request format") 186 } 187 tagPass := strings.SplitN(string(challenge), ":", 2) 188 if len(tagPass) != 2 { 189 return fmt.Errorf("invalid request format") 190 } 191 entity, err := checkCreds(h.state, params.Creds{ 192 AuthTag: tagPass[0], 193 Password: tagPass[1], 194 }) 195 if err != nil { 196 return err 197 } 198 // Only allow users, not agents. 199 _, _, err = names.ParseTag(entity.Tag(), names.UserTagKind) 200 if err != nil { 201 return common.ErrBadCreds 202 } 203 return err 204 } 205 206 // authError sends an unauthorized error. 207 func (h *charmsHandler) authError(w http.ResponseWriter) { 208 w.Header().Set("WWW-Authenticate", `Basic realm="juju"`) 209 h.sendError(w, http.StatusUnauthorized, "unauthorized") 210 } 211 212 // processPost handles a charm upload POST request after authentication. 213 func (h *charmsHandler) processPost(r *http.Request) (*charm.URL, error) { 214 query := r.URL.Query() 215 series := query.Get("series") 216 if series == "" { 217 return nil, fmt.Errorf("expected series=URL argument") 218 } 219 // Make sure the content type is zip. 220 contentType := r.Header.Get("Content-Type") 221 if contentType != "application/zip" { 222 return nil, fmt.Errorf("expected Content-Type: application/zip, got: %v", contentType) 223 } 224 tempFile, err := ioutil.TempFile("", "charm") 225 if err != nil { 226 return nil, fmt.Errorf("cannot create temp file: %v", err) 227 } 228 defer tempFile.Close() 229 defer os.Remove(tempFile.Name()) 230 if _, err := io.Copy(tempFile, r.Body); err != nil { 231 return nil, fmt.Errorf("error processing file upload: %v", err) 232 } 233 err = h.processUploadedArchive(tempFile.Name()) 234 if err != nil { 235 return nil, err 236 } 237 archive, err := charm.ReadBundle(tempFile.Name()) 238 if err != nil { 239 return nil, fmt.Errorf("invalid charm archive: %v", err) 240 } 241 // We got it, now let's reserve a charm URL for it in state. 242 archiveURL := &charm.URL{ 243 Schema: "local", 244 Series: series, 245 Name: archive.Meta().Name, 246 Revision: archive.Revision(), 247 } 248 preparedURL, err := h.state.PrepareLocalCharmUpload(archiveURL) 249 if err != nil { 250 return nil, err 251 } 252 // Now we need to repackage it with the reserved URL, upload it to 253 // provider storage and update the state. 254 err = h.repackageAndUploadCharm(archive, preparedURL) 255 if err != nil { 256 return nil, err 257 } 258 // All done. 259 return preparedURL, nil 260 } 261 262 // processUploadedArchive opens the given charm archive from path, 263 // inspects it to see if it has all files at the root of the archive 264 // or it has subdirs. It repackages the archive so it has all the 265 // files at the root dir, if necessary, replacing the original archive 266 // at path. 267 func (h *charmsHandler) processUploadedArchive(path string) error { 268 // Open the archive as a zip. 269 f, err := os.OpenFile(path, os.O_RDWR, 0644) 270 if err != nil { 271 return err 272 } 273 defer f.Close() 274 fi, err := f.Stat() 275 if err != nil { 276 return err 277 } 278 zipr, err := zip.NewReader(f, fi.Size()) 279 if err != nil { 280 return errgo.Annotate(err, "cannot open charm archive") 281 } 282 283 // Find out the root dir prefix from the archive. 284 rootDir, err := h.findArchiveRootDir(zipr) 285 if err != nil { 286 return errgo.Annotate(err, "cannot read charm archive") 287 } 288 if rootDir == "." { 289 // Normal charm, just use charm.ReadBundle(). 290 return nil 291 } 292 293 // There is one or more subdirs, so we need extract it to a temp 294 // dir and then read it as a charm dir. 295 tempDir, err := ioutil.TempDir("", "charm-extract") 296 if err != nil { 297 return errgo.Annotate(err, "cannot create temp directory") 298 } 299 defer os.RemoveAll(tempDir) 300 if err := ziputil.Extract(zipr, tempDir, rootDir); err != nil { 301 return errgo.Annotate(err, "cannot extract charm archive") 302 } 303 dir, err := charm.ReadDir(tempDir) 304 if err != nil { 305 return errgo.Annotate(err, "cannot read extracted archive") 306 } 307 308 // Now repackage the dir as a bundle at the original path. 309 if err := f.Truncate(0); err != nil { 310 return err 311 } 312 if err := dir.BundleTo(f); err != nil { 313 return err 314 } 315 return nil 316 } 317 318 // findArchiveRootDir scans a zip archive and returns the rootDir of 319 // the archive, the one containing metadata.yaml, config.yaml and 320 // revision files, or an error if the archive appears invalid. 321 func (h *charmsHandler) findArchiveRootDir(zipr *zip.Reader) (string, error) { 322 paths, err := ziputil.Find(zipr, "metadata.yaml") 323 if err != nil { 324 return "", err 325 } 326 switch len(paths) { 327 case 0: 328 return "", fmt.Errorf("invalid charm archive: missing metadata.yaml") 329 case 1: 330 default: 331 sort.Sort(byDepth(paths)) 332 if depth(paths[0]) == depth(paths[1]) { 333 return "", fmt.Errorf("invalid charm archive: ambiguous root directory") 334 } 335 } 336 return filepath.Dir(paths[0]), nil 337 } 338 339 func depth(path string) int { 340 return strings.Count(path, "/") 341 } 342 343 type byDepth []string 344 345 func (d byDepth) Len() int { return len(d) } 346 func (d byDepth) Swap(i, j int) { d[i], d[j] = d[j], d[i] } 347 func (d byDepth) Less(i, j int) bool { return depth(d[i]) < depth(d[j]) } 348 349 // repackageAndUploadCharm expands the given charm archive to a 350 // temporary directoy, repackages it with the given curl's revision, 351 // then uploads it to providr storage, and finally updates the state. 352 func (h *charmsHandler) repackageAndUploadCharm(archive *charm.Bundle, curl *charm.URL) error { 353 // Create a temp dir to contain the extracted charm 354 // dir and the repackaged archive. 355 tempDir, err := ioutil.TempDir("", "charm-download") 356 if err != nil { 357 return errgo.Annotate(err, "cannot create temp directory") 358 } 359 defer os.RemoveAll(tempDir) 360 extractPath := filepath.Join(tempDir, "extracted") 361 repackagedPath := filepath.Join(tempDir, "repackaged.zip") 362 repackagedArchive, err := os.Create(repackagedPath) 363 if err != nil { 364 return errgo.Annotate(err, "cannot repackage uploaded charm") 365 } 366 defer repackagedArchive.Close() 367 368 // Expand and repack it with the revision specified by curl. 369 archive.SetRevision(curl.Revision) 370 if err := archive.ExpandTo(extractPath); err != nil { 371 return errgo.Annotate(err, "cannot extract uploaded charm") 372 } 373 charmDir, err := charm.ReadDir(extractPath) 374 if err != nil { 375 return errgo.Annotate(err, "cannot read extracted charm") 376 } 377 378 // Bundle the charm and calculate its sha256 hash at the 379 // same time. 380 hash := sha256.New() 381 err = charmDir.BundleTo(io.MultiWriter(hash, repackagedArchive)) 382 if err != nil { 383 return errgo.Annotate(err, "cannot repackage uploaded charm") 384 } 385 bundleSHA256 := hex.EncodeToString(hash.Sum(nil)) 386 size, err := repackagedArchive.Seek(0, 2) 387 if err != nil { 388 return errgo.Annotate(err, "cannot get charm file size") 389 } 390 391 // Now upload to provider storage. 392 if _, err := repackagedArchive.Seek(0, 0); err != nil { 393 return errgo.Annotate(err, "cannot rewind the charm file reader") 394 } 395 storage, err := envtesting.GetEnvironStorage(h.state) 396 if err != nil { 397 return errgo.Annotate(err, "cannot access provider storage") 398 } 399 name := charm.Quote(curl.String()) 400 if err := storage.Put(name, repackagedArchive, size); err != nil { 401 return errgo.Annotate(err, "cannot upload charm to provider storage") 402 } 403 storageURL, err := storage.URL(name) 404 if err != nil { 405 return errgo.Annotate(err, "cannot get storage URL for charm") 406 } 407 bundleURL, err := url.Parse(storageURL) 408 if err != nil { 409 return errgo.Annotate(err, "cannot parse storage URL") 410 } 411 412 // And finally, update state. 413 _, err = h.state.UpdateUploadedCharm(archive, curl, bundleURL, bundleSHA256) 414 if err != nil { 415 return errgo.Annotate(err, "cannot update uploaded charm in state") 416 } 417 return nil 418 } 419 420 // processGet handles a charm file GET request after authentication. 421 // It returns the bundle path, the requested file path (if any) and an error. 422 func (h *charmsHandler) processGet(r *http.Request) (string, string, error) { 423 query := r.URL.Query() 424 425 // Retrieve and validate query parameters. 426 curl := query.Get("url") 427 if curl == "" { 428 return "", "", fmt.Errorf("expected url=CharmURL query argument") 429 } 430 var filePath string 431 file := query.Get("file") 432 if file == "" { 433 filePath = "" 434 } else { 435 filePath = path.Clean(file) 436 } 437 438 // Prepare the bundle directories. 439 name := charm.Quote(curl) 440 charmArchivePath := filepath.Join(h.dataDir, "charm-get-cache", name+".zip") 441 442 // Check if the charm archive is already in the cache. 443 if _, err := os.Stat(charmArchivePath); os.IsNotExist(err) { 444 // Download the charm archive and save it to the cache. 445 if err = h.downloadCharm(name, charmArchivePath); err != nil { 446 return "", "", fmt.Errorf("unable to retrieve and save the charm: %v", err) 447 } 448 } else if err != nil { 449 return "", "", fmt.Errorf("cannot access the charms cache: %v", err) 450 } 451 return charmArchivePath, filePath, nil 452 } 453 454 // downloadCharm downloads the given charm name from the provider storage and 455 // saves the corresponding zip archive to the given charmArchivePath. 456 func (h *charmsHandler) downloadCharm(name, charmArchivePath string) error { 457 // Get the provider storage. 458 storage, err := envtesting.GetEnvironStorage(h.state) 459 if err != nil { 460 return errgo.Annotate(err, "cannot access provider storage") 461 } 462 463 // Use the storage to retrieve and save the charm archive. 464 reader, err := storage.Get(name) 465 if err != nil { 466 return errgo.Annotate(err, "charm not found in the provider storage") 467 } 468 defer reader.Close() 469 data, err := ioutil.ReadAll(reader) 470 if err != nil { 471 return errgo.Annotate(err, "cannot read charm data") 472 } 473 // In order to avoid races, the archive is saved in a temporary file which 474 // is then atomically renamed. The temporary file is created in the 475 // charm cache directory so that we can safely assume the rename source and 476 // target live in the same file system. 477 cacheDir := filepath.Dir(charmArchivePath) 478 if err = os.MkdirAll(cacheDir, 0755); err != nil { 479 return errgo.Annotate(err, "cannot create the charms cache") 480 } 481 tempCharmArchive, err := ioutil.TempFile(cacheDir, "charm") 482 if err != nil { 483 return errgo.Annotate(err, "cannot create charm archive temp file") 484 } 485 defer tempCharmArchive.Close() 486 if err = ioutil.WriteFile(tempCharmArchive.Name(), data, 0644); err != nil { 487 return errgo.Annotate(err, "error processing charm archive download") 488 } 489 if err = os.Rename(tempCharmArchive.Name(), charmArchivePath); err != nil { 490 defer os.Remove(tempCharmArchive.Name()) 491 return errgo.Annotate(err, "error renaming the charm archive") 492 } 493 return nil 494 }