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