github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/apiserver/tools.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  	"bytes"
     8  	"crypto/sha256"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"net/http"
    13  	"net/url"
    14  	"strings"
    15  
    16  	"github.com/juju/errors"
    17  	"github.com/juju/utils"
    18  	"github.com/juju/version"
    19  
    20  	"github.com/juju/juju/apiserver/common"
    21  	"github.com/juju/juju/apiserver/params"
    22  	"github.com/juju/juju/environs"
    23  	envtools "github.com/juju/juju/environs/tools"
    24  	"github.com/juju/juju/state"
    25  	"github.com/juju/juju/state/binarystorage"
    26  	"github.com/juju/juju/state/stateenvirons"
    27  	"github.com/juju/juju/tools"
    28  )
    29  
    30  // toolsHandler handles tool upload through HTTPS in the API server.
    31  type toolsUploadHandler struct {
    32  	ctxt httpContext
    33  }
    34  
    35  // toolsHandler handles tool download through HTTPS in the API server.
    36  type toolsDownloadHandler struct {
    37  	ctxt httpContext
    38  }
    39  
    40  func (h *toolsDownloadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    41  	st, err := h.ctxt.stateForRequestUnauthenticated(r)
    42  	if err != nil {
    43  		if err := sendError(w, err); err != nil {
    44  			logger.Errorf("%v", err)
    45  		}
    46  		return
    47  	}
    48  
    49  	switch r.Method {
    50  	case "GET":
    51  		tarball, err := h.processGet(r, st)
    52  		if err != nil {
    53  			logger.Errorf("GET(%s) failed: %v", r.URL, err)
    54  			if err := sendError(w, errors.NewBadRequest(err, "")); err != nil {
    55  				logger.Errorf("%v", err)
    56  			}
    57  			return
    58  		}
    59  		if err := h.sendTools(w, http.StatusOK, tarball); err != nil {
    60  			logger.Errorf("%v", err)
    61  		}
    62  	default:
    63  		if err := sendError(w, errors.MethodNotAllowedf("unsupported method: %q", r.Method)); err != nil {
    64  			logger.Errorf("%v", err)
    65  		}
    66  	}
    67  }
    68  
    69  func (h *toolsUploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    70  	// Validate before authenticate because the authentication is dependent
    71  	// on the state connection that is determined during the validation.
    72  	st, _, err := h.ctxt.stateForRequestAuthenticatedUser(r)
    73  	if err != nil {
    74  		if err := sendError(w, err); err != nil {
    75  			logger.Errorf("%v", err)
    76  		}
    77  		return
    78  	}
    79  
    80  	switch r.Method {
    81  	case "POST":
    82  		// Add tools to storage.
    83  		agentTools, err := h.processPost(r, st)
    84  		if err != nil {
    85  			if err := sendError(w, err); err != nil {
    86  				logger.Errorf("%v", err)
    87  			}
    88  			return
    89  		}
    90  		if err := sendStatusAndJSON(w, http.StatusOK, &params.ToolsResult{
    91  			ToolsList: tools.List{agentTools},
    92  		}); err != nil {
    93  			logger.Errorf("%v", err)
    94  		}
    95  	default:
    96  		if err := sendError(w, errors.MethodNotAllowedf("unsupported method: %q", r.Method)); err != nil {
    97  			logger.Errorf("%v", err)
    98  		}
    99  	}
   100  }
   101  
   102  // processGet handles a tools GET request.
   103  func (h *toolsDownloadHandler) processGet(r *http.Request, st *state.State) ([]byte, error) {
   104  	version, err := version.ParseBinary(r.URL.Query().Get(":version"))
   105  	if err != nil {
   106  		return nil, errors.Annotate(err, "error parsing version")
   107  	}
   108  	storage, err := st.ToolsStorage()
   109  	if err != nil {
   110  		return nil, errors.Annotate(err, "error getting tools storage")
   111  	}
   112  	defer storage.Close()
   113  	_, reader, err := storage.Open(version.String())
   114  	if errors.IsNotFound(err) {
   115  		// Tools could not be found in tools storage,
   116  		// so look for them in simplestreams, fetch
   117  		// them and cache in tools storage.
   118  		logger.Infof("%v tools not found locally, fetching", version)
   119  		reader, err = h.fetchAndCacheTools(version, storage, st)
   120  		if err != nil {
   121  			err = errors.Annotate(err, "error fetching tools")
   122  		}
   123  	}
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	defer reader.Close()
   128  	data, err := ioutil.ReadAll(reader)
   129  	if err != nil {
   130  		return nil, errors.Annotate(err, "failed to read tools tarball")
   131  	}
   132  	return data, nil
   133  }
   134  
   135  // fetchAndCacheTools fetches tools with the specified version by searching for a URL
   136  // in simplestreams and GETting it, caching the result in tools storage before returning
   137  // to the caller.
   138  func (h *toolsDownloadHandler) fetchAndCacheTools(v version.Binary, stor binarystorage.Storage, st *state.State) (io.ReadCloser, error) {
   139  	newEnviron := stateenvirons.GetNewEnvironFunc(environs.New)
   140  	env, err := newEnviron(st)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  	tools, err := envtools.FindExactTools(env, v.Number, v.Series, v.Arch)
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  
   149  	// No need to verify the server's identity because we verify the SHA-256 hash.
   150  	logger.Infof("fetching %v tools from %v", v, tools.URL)
   151  	resp, err := utils.GetNonValidatingHTTPClient().Get(tools.URL)
   152  	if err != nil {
   153  		return nil, err
   154  	}
   155  	defer resp.Body.Close()
   156  	if resp.StatusCode != http.StatusOK {
   157  		msg := fmt.Sprintf("bad HTTP response: %v", resp.Status)
   158  		if body, err := ioutil.ReadAll(resp.Body); err == nil {
   159  			msg += fmt.Sprintf(" (%s)", bytes.TrimSpace(body))
   160  		}
   161  		return nil, errors.New(msg)
   162  	}
   163  	data, sha256, err := readAndHash(resp.Body)
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  	if int64(len(data)) != tools.Size {
   168  		return nil, errors.Errorf("size mismatch for %s", tools.URL)
   169  	}
   170  	if sha256 != tools.SHA256 {
   171  		return nil, errors.Errorf("hash mismatch for %s", tools.URL)
   172  	}
   173  
   174  	// Cache tarball in tools storage before returning.
   175  	metadata := binarystorage.Metadata{
   176  		Version: v.String(),
   177  		Size:    tools.Size,
   178  		SHA256:  tools.SHA256,
   179  	}
   180  	if err := stor.Add(bytes.NewReader(data), metadata); err != nil {
   181  		return nil, errors.Annotate(err, "error caching tools")
   182  	}
   183  	return ioutil.NopCloser(bytes.NewReader(data)), nil
   184  }
   185  
   186  // sendTools streams the tools tarball to the client.
   187  func (h *toolsDownloadHandler) sendTools(w http.ResponseWriter, statusCode int, tarball []byte) error {
   188  	w.Header().Set("Content-Type", "application/x-tar-gz")
   189  	w.Header().Set("Content-Length", fmt.Sprint(len(tarball)))
   190  	w.WriteHeader(statusCode)
   191  	if _, err := w.Write(tarball); err != nil {
   192  		return errors.Trace(sendError(
   193  			w,
   194  			errors.NewBadRequest(errors.Annotatef(err, "failed to write tools"), ""),
   195  		))
   196  	}
   197  	return nil
   198  }
   199  
   200  // processPost handles a tools upload POST request after authentication.
   201  func (h *toolsUploadHandler) processPost(r *http.Request, st *state.State) (*tools.Tools, error) {
   202  	query := r.URL.Query()
   203  
   204  	binaryVersionParam := query.Get("binaryVersion")
   205  	if binaryVersionParam == "" {
   206  		return nil, errors.BadRequestf("expected binaryVersion argument")
   207  	}
   208  	toolsVersion, err := version.ParseBinary(binaryVersionParam)
   209  	if err != nil {
   210  		return nil, errors.NewBadRequest(err, fmt.Sprintf("invalid tools version %q", binaryVersionParam))
   211  	}
   212  
   213  	// Make sure the content type is x-tar-gz.
   214  	contentType := r.Header.Get("Content-Type")
   215  	if contentType != "application/x-tar-gz" {
   216  		return nil, errors.BadRequestf("expected Content-Type: application/x-tar-gz, got: %v", contentType)
   217  	}
   218  
   219  	// Get the server root, so we know how to form the URL in the Tools returned.
   220  	serverRoot, err := h.getServerRoot(r, query, st)
   221  	if err != nil {
   222  		return nil, errors.NewBadRequest(err, "cannot to determine server root")
   223  	}
   224  
   225  	// We'll clone the tools for each additional series specified.
   226  	var cloneSeries []string
   227  	if seriesParam := query.Get("series"); seriesParam != "" {
   228  		cloneSeries = strings.Split(seriesParam, ",")
   229  	}
   230  	logger.Debugf("request to upload tools: %s", toolsVersion)
   231  	logger.Debugf("additional series: %s", cloneSeries)
   232  
   233  	toolsVersions := []version.Binary{toolsVersion}
   234  	for _, series := range cloneSeries {
   235  		if series != toolsVersion.Series {
   236  			v := toolsVersion
   237  			v.Series = series
   238  			toolsVersions = append(toolsVersions, v)
   239  		}
   240  	}
   241  	return h.handleUpload(r.Body, toolsVersions, serverRoot, st)
   242  }
   243  
   244  func (h *toolsUploadHandler) getServerRoot(r *http.Request, query url.Values, st *state.State) (string, error) {
   245  	uuid := query.Get(":modeluuid")
   246  	if uuid == "" {
   247  		env, err := st.Model()
   248  		if err != nil {
   249  			return "", err
   250  		}
   251  		uuid = env.UUID()
   252  	}
   253  	return fmt.Sprintf("https://%s/model/%s", r.Host, uuid), nil
   254  }
   255  
   256  // handleUpload uploads the tools data from the reader to env storage as the specified version.
   257  func (h *toolsUploadHandler) handleUpload(r io.Reader, toolsVersions []version.Binary, serverRoot string, st *state.State) (*tools.Tools, error) {
   258  	// Check if changes are allowed and the command may proceed.
   259  	blockChecker := common.NewBlockChecker(st)
   260  	if err := blockChecker.ChangeAllowed(); err != nil {
   261  		return nil, errors.Trace(err)
   262  	}
   263  	storage, err := st.ToolsStorage()
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  	defer storage.Close()
   268  
   269  	// Read the tools tarball from the request, calculating the sha256 along the way.
   270  	data, sha256, err := readAndHash(r)
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  	if len(data) == 0 {
   275  		return nil, errors.BadRequestf("no tools uploaded")
   276  	}
   277  
   278  	// TODO(wallyworld): check integrity of tools tarball.
   279  
   280  	// Store tools and metadata in tools storage.
   281  	for _, v := range toolsVersions {
   282  		metadata := binarystorage.Metadata{
   283  			Version: v.String(),
   284  			Size:    int64(len(data)),
   285  			SHA256:  sha256,
   286  		}
   287  		logger.Debugf("uploading tools %+v to storage", metadata)
   288  		if err := storage.Add(bytes.NewReader(data), metadata); err != nil {
   289  			return nil, err
   290  		}
   291  	}
   292  
   293  	tools := &tools.Tools{
   294  		Version: toolsVersions[0],
   295  		Size:    int64(len(data)),
   296  		SHA256:  sha256,
   297  		URL:     common.ToolsURL(serverRoot, toolsVersions[0]),
   298  	}
   299  	return tools, nil
   300  }
   301  
   302  func readAndHash(r io.Reader) (data []byte, sha256hex string, err error) {
   303  	hash := sha256.New()
   304  	data, err = ioutil.ReadAll(io.TeeReader(r, hash))
   305  	if err != nil {
   306  		return nil, "", errors.Annotate(err, "error processing file upload")
   307  	}
   308  	return data, fmt.Sprintf("%x", hash.Sum(nil)), nil
   309  }