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, ¶ms.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 }