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