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, &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  // 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  }