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