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