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, &params.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, &params.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, &params.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  }