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