launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/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  	"net/http"
    16  	"net/url"
    17  	"os"
    18  	"path/filepath"
    19  	"strings"
    20  
    21  	"launchpad.net/errgo/errors"
    22  	"launchpad.net/juju-core/charm"
    23  	envtesting "launchpad.net/juju-core/environs/testing"
    24  	"launchpad.net/juju-core/names"
    25  	"launchpad.net/juju-core/state"
    26  	"launchpad.net/juju-core/state/api/params"
    27  	"launchpad.net/juju-core/state/apiserver/common"
    28  )
    29  
    30  // charmsHandler handles charm upload through HTTPS in the API server.
    31  type charmsHandler struct {
    32  	state *state.State
    33  }
    34  
    35  func (h *charmsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    36  	if err := h.authenticate(r); err != nil {
    37  		h.authError(w)
    38  		return
    39  	}
    40  
    41  	switch r.Method {
    42  	case "POST":
    43  		charmURL, err := h.processPost(r)
    44  		if err != nil {
    45  			h.sendError(w, http.StatusBadRequest, err.Error())
    46  			return
    47  		}
    48  		h.sendJSON(w, http.StatusOK, &params.CharmsResponse{CharmURL: charmURL.String()})
    49  	// Possible future extensions, like GET.
    50  	default:
    51  		h.sendError(w, http.StatusMethodNotAllowed, fmt.Sprintf("unsupported method: %q", r.Method))
    52  	}
    53  }
    54  
    55  // sendJSON sends a JSON-encoded response to the client.
    56  func (h *charmsHandler) sendJSON(w http.ResponseWriter, statusCode int, response *params.CharmsResponse) error {
    57  	w.WriteHeader(statusCode)
    58  	body, err := json.Marshal(response)
    59  	if err != nil {
    60  		return mask(err)
    61  	}
    62  	w.Write(body)
    63  	return nil
    64  }
    65  
    66  // sendError sends a JSON-encoded error response.
    67  func (h *charmsHandler) sendError(w http.ResponseWriter, statusCode int, message string) error {
    68  	return h.sendJSON(w, statusCode, &params.CharmsResponse{Error: message})
    69  }
    70  
    71  // authenticate parses HTTP basic authentication and authorizes the
    72  // request by looking up the provided tag and password against state.
    73  func (h *charmsHandler) authenticate(r *http.Request) error {
    74  	parts := strings.Fields(r.Header.Get("Authorization"))
    75  	if len(parts) != 2 || parts[0] != "Basic" {
    76  		// Invalid header format or no header provided.
    77  		return errors.Newf("invalid request format")
    78  	}
    79  	// Challenge is a base64-encoded "tag:pass" string.
    80  	// See RFC 2617, Section 2.
    81  	challenge, err := base64.StdEncoding.DecodeString(parts[1])
    82  	if err != nil {
    83  		return errors.Newf("invalid request format")
    84  	}
    85  	tagPass := strings.SplitN(string(challenge), ":", 2)
    86  	if len(tagPass) != 2 {
    87  		return errors.Newf("invalid request format")
    88  	}
    89  	entity, err := checkCreds(h.state, params.Creds{
    90  		AuthTag:  tagPass[0],
    91  		Password: tagPass[1],
    92  	})
    93  	if err != nil {
    94  		return mask(err)
    95  	}
    96  
    97  	// Only allow users, not agents.
    98  	_, _, err = names.ParseTag(entity.Tag(), names.UserTagKind)
    99  	if err != nil {
   100  		return common.ErrBadCreds
   101  	}
   102  	return err
   103  }
   104  
   105  // authError sends an unauthorized error.
   106  func (h *charmsHandler) authError(w http.ResponseWriter) {
   107  	w.Header().Set("WWW-Authenticate", `Basic realm="juju"`)
   108  	h.sendError(w, http.StatusUnauthorized, "unauthorized")
   109  }
   110  
   111  // processPost handles a charm upload POST request after authentication.
   112  func (h *charmsHandler) processPost(r *http.Request) (*charm.URL, error) {
   113  	query := r.URL.Query()
   114  	series := query.Get("series")
   115  	if series == "" {
   116  		return nil, errors.Newf("expected series= URL argument")
   117  	}
   118  	// Make sure the content type is zip.
   119  	contentType := r.Header.Get("Content-Type")
   120  	if contentType != "application/zip" {
   121  		return nil, errors.Newf("expected Content-Type: application/zip, got: %v", contentType)
   122  	}
   123  	tempFile, err := ioutil.TempFile("", "charm")
   124  	if err != nil {
   125  		return nil, errors.Notef(err, "cannot create temp file")
   126  	}
   127  	defer tempFile.Close()
   128  	defer os.Remove(tempFile.Name())
   129  	if _, err := io.Copy(tempFile, r.Body); err != nil {
   130  		return nil, errors.Notef(err, "error processing file upload")
   131  	}
   132  	err = h.processUploadedArchive(tempFile.Name())
   133  	if err != nil {
   134  		return nil, mask(err)
   135  	}
   136  	archive, err := charm.ReadBundle(tempFile.Name())
   137  	if err != nil {
   138  		return nil, errors.Notef(err, "invalid charm archive")
   139  	}
   140  	// We got it, now let's reserve a charm URL for it in state.
   141  	archiveURL := &charm.URL{
   142  		Schema:   "local",
   143  		Series:   series,
   144  		Name:     archive.Meta().Name,
   145  		Revision: archive.Revision(),
   146  	}
   147  	preparedURL, err := h.state.PrepareLocalCharmUpload(archiveURL)
   148  	if err != nil {
   149  		return nil, mask(err)
   150  	}
   151  
   152  	// Now we need to repackage it with the reserved URL, upload it to
   153  	// provider storage and update the state.
   154  	err = h.repackageAndUploadCharm(archive, preparedURL)
   155  	if err != nil {
   156  		return nil, mask(err)
   157  	}
   158  
   159  	// All done.
   160  	return preparedURL, nil
   161  }
   162  
   163  // processUploadedArchive opens the given charm archive from path,
   164  // inspects it to see if it has all files at the root of the archive
   165  // or it has subdirs. It repackages the archive so it has all the
   166  // files at the root dir, if necessary, replacing the original archive
   167  // at path.
   168  func (h *charmsHandler) processUploadedArchive(path string) error {
   169  	// Open the archive as a zip.
   170  	f, err := os.OpenFile(path, os.O_RDWR, 0644)
   171  	if err != nil {
   172  		return mask(err)
   173  	}
   174  	defer f.Close()
   175  	fi, err := f.Stat()
   176  	if err != nil {
   177  		return mask(err)
   178  	}
   179  	zipr, err := zip.NewReader(f, fi.Size())
   180  	if err != nil {
   181  		return errors.NoteMask(err, "cannot open charm archive")
   182  	}
   183  
   184  	// Find out the root dir prefix from the archive.
   185  	rootDir, err := h.findArchiveRootDir(zipr)
   186  	if err != nil {
   187  		return errors.NoteMask(err, "cannot read charm archive")
   188  	}
   189  	if rootDir == "" {
   190  		// Normal charm, just use charm.ReadBundle().
   191  		return nil
   192  	}
   193  	// There is one or more subdirs, so we need extract it to a temp
   194  	// dir and then read is as a charm dir.
   195  	tempDir, err := ioutil.TempDir("", "charm-extract")
   196  	if err != nil {
   197  		return errors.NoteMask(err, "cannot create temp directory")
   198  	}
   199  	defer os.RemoveAll(tempDir)
   200  	err = h.extractArchiveTo(zipr, rootDir, tempDir)
   201  	if err != nil {
   202  		return errors.NoteMask(err, "cannot extract charm archive")
   203  	}
   204  	dir, err := charm.ReadDir(tempDir)
   205  	if err != nil {
   206  		return errors.NoteMask(err, "cannot read extracted archive")
   207  	}
   208  	// Now repackage the dir as a bundle at the original path.
   209  	if err := f.Truncate(0); err != nil {
   210  		return mask(err)
   211  	}
   212  	if err := dir.BundleTo(f); err != nil {
   213  		return mask(err)
   214  	}
   215  	return nil
   216  }
   217  
   218  // fixPath converts all forward and backslashes in path to the OS path
   219  // separator and calls filepath.Clean before returning it.
   220  func (h *charmsHandler) fixPath(path string) string {
   221  	sep := string(filepath.Separator)
   222  	p := strings.Replace(path, "\\", sep, -1)
   223  	return filepath.Clean(strings.Replace(p, "/", sep, -1))
   224  }
   225  
   226  // findArchiveRootDir scans a zip archive and returns the rootDir of
   227  // the archive, the one containing metadata.yaml, config.yaml and
   228  // revision files, or an error if the archive appears invalid.
   229  func (h *charmsHandler) findArchiveRootDir(zipr *zip.Reader) (string, error) {
   230  	numFound := 0
   231  	metadataFound := false // metadata.yaml is the only required file.
   232  	rootPath := ""
   233  	lookFor := []string{"metadata.yaml", "config.yaml", "revision"}
   234  	for _, fh := range zipr.File {
   235  		for _, fname := range lookFor {
   236  			dir, file := filepath.Split(h.fixPath(fh.Name))
   237  			if file == fname {
   238  				if file == "metadata.yaml" {
   239  					metadataFound = true
   240  				}
   241  				numFound++
   242  				if rootPath == "" {
   243  					rootPath = dir
   244  				} else if rootPath != dir {
   245  					return "", errors.Newf("invalid charm archive: expected all %v files in the same directory", lookFor)
   246  				}
   247  				if numFound == len(lookFor) {
   248  					return rootPath, nil
   249  				}
   250  			}
   251  		}
   252  	}
   253  	if !metadataFound {
   254  		return "", errors.Newf("invalid charm archive: missing metadata.yaml")
   255  	}
   256  	return rootPath, nil
   257  }
   258  
   259  // extractArchiveTo extracts an archive to the given destDir, removing
   260  // the rootDir from each file, effectively reducing any nested subdirs
   261  // to the root level.
   262  func (h *charmsHandler) extractArchiveTo(zipr *zip.Reader, rootDir, destDir string) error {
   263  	for _, fh := range zipr.File {
   264  		err := h.extractSingleFile(fh, rootDir, destDir)
   265  		if err != nil {
   266  			return mask(err)
   267  		}
   268  	}
   269  	return nil
   270  }
   271  
   272  // extractSingleFile extracts the given zip file header, removing
   273  // rootDir from the filename, to the destDir.
   274  func (h *charmsHandler) extractSingleFile(fh *zip.File, rootDir, destDir string) error {
   275  	cleanName := h.fixPath(fh.Name)
   276  	relName, err := filepath.Rel(rootDir, cleanName)
   277  	if err != nil {
   278  		// Skip paths not relative to roo
   279  		return nil
   280  	}
   281  	if strings.Contains(relName, "..") || relName == "." {
   282  		// Skip current dir and paths outside rootDir.
   283  		return nil
   284  	}
   285  	dirName := filepath.Dir(relName)
   286  	f, err := fh.Open()
   287  	if err != nil {
   288  		return mask(err)
   289  	}
   290  	defer f.Close()
   291  
   292  	mode := fh.Mode()
   293  	destPath := filepath.Join(destDir, relName)
   294  	if dirName != "" && mode&os.ModeDir != 0 {
   295  		err = os.MkdirAll(destPath, mode&0777)
   296  		if err != nil {
   297  			return mask(err)
   298  		}
   299  		return nil
   300  	}
   301  
   302  	if mode&os.ModeSymlink != 0 {
   303  		data, err := ioutil.ReadAll(f)
   304  		if err != nil {
   305  			return mask(err)
   306  		}
   307  		target := string(data)
   308  		if filepath.IsAbs(target) {
   309  			return errors.Newf("symlink %q is absolute: %q", cleanName, target)
   310  		}
   311  		p := filepath.Join(dirName, target)
   312  		if strings.Contains(p, "..") {
   313  			return errors.Newf("symlink %q links out of charm: %s", cleanName, target)
   314  		}
   315  		err = os.Symlink(target, destPath)
   316  		if err != nil {
   317  			return mask(err)
   318  		}
   319  	}
   320  	if dirName == "hooks" {
   321  		if mode&os.ModeType == 0 {
   322  			// Set all hooks executable (by owner)
   323  			mode = mode | 0100
   324  		}
   325  	}
   326  
   327  	// Check file type.
   328  	e := "file has an unknown type: %q"
   329  	switch mode & os.ModeType {
   330  	case os.ModeDir, os.ModeSymlink, 0:
   331  		// That's expected, it's ok.
   332  		e = ""
   333  	case os.ModeNamedPipe:
   334  		e = "file is a named pipe: %q"
   335  	case os.ModeSocket:
   336  		e = "file is a socket: %q"
   337  	case os.ModeDevice:
   338  		e = "file is a device: %q"
   339  	}
   340  	if e != "" {
   341  		return errors.Newf(e, destPath)
   342  	}
   343  
   344  	out, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY, mode&0777)
   345  	if err != nil {
   346  		return errors.Notef(err, "creating %q failed", destPath)
   347  	}
   348  	defer out.Close()
   349  	_, err = io.Copy(out, f)
   350  	return err
   351  }
   352  
   353  // repackageAndUploadCharm expands the given charm archive to a
   354  // temporary directoy, repackages it with the given curl's revision,
   355  // then uploads it to providr storage, and finally updates the state.
   356  func (h *charmsHandler) repackageAndUploadCharm(archive *charm.Bundle, curl *charm.URL) error {
   357  	// Create a temp dir to contain the extracted charm
   358  	// dir and the repackaged archive.
   359  	tempDir, err := ioutil.TempDir("", "charm-download")
   360  	if err != nil {
   361  		return errors.NoteMask(err, "cannot create temp directory")
   362  	}
   363  	defer os.RemoveAll(tempDir)
   364  	extractPath := filepath.Join(tempDir, "extracted")
   365  	repackagedPath := filepath.Join(tempDir, "repackaged.zip")
   366  	repackagedArchive, err := os.Create(repackagedPath)
   367  	if err != nil {
   368  		return errors.NoteMask(err, "cannot repackage uploaded charm")
   369  	}
   370  	defer repackagedArchive.Close()
   371  
   372  	// Expand and repack it with the revision specified by curl.
   373  	archive.SetRevision(curl.Revision)
   374  	if err := archive.ExpandTo(extractPath); err != nil {
   375  		return errors.NoteMask(err, "cannot extract uploaded charm")
   376  	}
   377  	charmDir, err := charm.ReadDir(extractPath)
   378  	if err != nil {
   379  		return errors.NoteMask(err, "cannot read extracted charm")
   380  	}
   381  	// Bundle the charm and calculate its sha256 hash at the
   382  	// same time.
   383  	hash := sha256.New()
   384  	err = charmDir.BundleTo(io.MultiWriter(hash, repackagedArchive))
   385  	if err != nil {
   386  		return errors.NoteMask(err, "cannot repackage uploaded charm")
   387  	}
   388  	bundleSHA256 := hex.EncodeToString(hash.Sum(nil))
   389  	size, err := repackagedArchive.Seek(0, 2)
   390  	if err != nil {
   391  		return errors.NoteMask(err, "cannot get charm file size")
   392  	}
   393  	// Seek to the beginning so the subsequent Put will read
   394  	// the whole file again.
   395  	if _, err := repackagedArchive.Seek(0, 0); err != nil {
   396  		return errors.NoteMask(err, "cannot rewind the charm file reader")
   397  	}
   398  
   399  	// Now upload to provider storage.
   400  	storage, err := envtesting.GetEnvironStorage(h.state)
   401  	if err != nil {
   402  		return errors.NoteMask(err, "cannot access provider storage")
   403  	}
   404  	name := charm.Quote(curl.String())
   405  	if err := storage.Put(name, repackagedArchive, size); err != nil {
   406  		return errors.NoteMask(err, "cannot upload charm to provider storage")
   407  	}
   408  	storageURL, err := storage.URL(name)
   409  	if err != nil {
   410  		return errors.NoteMask(err, "cannot get storage URL for charm")
   411  	}
   412  	bundleURL, err := url.Parse(storageURL)
   413  	if err != nil {
   414  		return errors.NoteMask(err, "cannot parse storage URL")
   415  	}
   416  
   417  	// And finally, update state.
   418  	_, err = h.state.UpdateUploadedCharm(archive, curl, bundleURL, bundleSHA256)
   419  	if err != nil {
   420  		return errors.NoteMask(err, "cannot update uploaded charm in state")
   421  	}
   422  	return nil
   423  }