OLD | NEW |
1 // Copyright 2013 Canonical Ltd. | 1 // Copyright 2013 Canonical Ltd. |
2 // Licensed under the AGPLv3, see LICENCE file for details. | 2 // Licensed under the AGPLv3, see LICENCE file for details. |
3 | 3 |
4 package apiserver | 4 package apiserver |
5 | 5 |
6 import ( | 6 import ( |
7 "archive/zip" | 7 "archive/zip" |
8 "crypto/sha256" | 8 "crypto/sha256" |
9 "encoding/base64" | 9 "encoding/base64" |
10 "encoding/hex" | 10 "encoding/hex" |
(...skipping 12 matching lines...) Expand all Loading... |
23 "launchpad.net/juju-core/charm" | 23 "launchpad.net/juju-core/charm" |
24 envtesting "launchpad.net/juju-core/environs/testing" | 24 envtesting "launchpad.net/juju-core/environs/testing" |
25 "launchpad.net/juju-core/names" | 25 "launchpad.net/juju-core/names" |
26 "launchpad.net/juju-core/state" | 26 "launchpad.net/juju-core/state" |
27 "launchpad.net/juju-core/state/api/params" | 27 "launchpad.net/juju-core/state/api/params" |
28 "launchpad.net/juju-core/state/apiserver/common" | 28 "launchpad.net/juju-core/state/apiserver/common" |
29 ) | 29 ) |
30 | 30 |
31 // charmsHandler handles charm upload through HTTPS in the API server. | 31 // charmsHandler handles charm upload through HTTPS in the API server. |
32 type charmsHandler struct { | 32 type charmsHandler struct { |
33 » state *state.State | 33 » state *state.State |
| 34 » dataDir string |
34 } | 35 } |
35 | 36 |
36 func (h *charmsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | 37 func (h *charmsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
37 if err := h.authenticate(r); err != nil { | 38 if err := h.authenticate(r); err != nil { |
38 h.authError(w) | 39 h.authError(w) |
39 return | 40 return |
40 } | 41 } |
41 | 42 |
42 switch r.Method { | 43 switch r.Method { |
43 case "POST": | 44 case "POST": |
| 45 // Add a local charm to the store provider. |
| 46 // Requires a "series" query specifying the series to use for th
e charm. |
44 charmURL, err := h.processPost(r) | 47 charmURL, err := h.processPost(r) |
45 if err != nil { | 48 if err != nil { |
46 h.sendError(w, http.StatusBadRequest, err.Error()) | 49 h.sendError(w, http.StatusBadRequest, err.Error()) |
47 return | 50 return |
48 } | 51 } |
49 h.sendJSON(w, http.StatusOK, ¶ms.CharmsResponse{CharmURL: ch
armURL.String()}) | 52 h.sendJSON(w, http.StatusOK, ¶ms.CharmsResponse{CharmURL: ch
armURL.String()}) |
50 » // Possible future extensions, like GET. | 53 » case "GET": |
| 54 » » // Retrieve or list charm files. |
| 55 » » // Requires "url" (charm URL) and an optional "file" (the path t
o the |
| 56 » » // charm file) to be included in the query. |
| 57 » » if bundlePath, filePath, err := h.processGet(r); err != nil { |
| 58 » » » // An error occurred retrieving the charm bundle. |
| 59 » » » h.sendError(w, http.StatusBadRequest, err.Error()) |
| 60 » » } else if filePath == "" { |
| 61 » » » // The client requested the list of charm files. |
| 62 » » » h.sendFilesList(w, bundlePath) |
| 63 » » } else { |
| 64 » » » // The client requested a specific file. |
| 65 » » » h.sendFile(w, r, filePath) |
| 66 » » } |
51 default: | 67 default: |
52 h.sendError(w, http.StatusMethodNotAllowed, fmt.Sprintf("unsuppo
rted method: %q", r.Method)) | 68 h.sendError(w, http.StatusMethodNotAllowed, fmt.Sprintf("unsuppo
rted method: %q", r.Method)) |
53 } | 69 } |
54 } | 70 } |
55 | 71 |
56 // sendJSON sends a JSON-encoded response to the client. | 72 // sendJSON sends a JSON-encoded response to the client. |
57 func (h *charmsHandler) sendJSON(w http.ResponseWriter, statusCode int, response
*params.CharmsResponse) error { | 73 func (h *charmsHandler) sendJSON(w http.ResponseWriter, statusCode int, response
*params.CharmsResponse) error { |
| 74 w.Header().Set("Content-Type", "application/json") |
58 w.WriteHeader(statusCode) | 75 w.WriteHeader(statusCode) |
59 body, err := json.Marshal(response) | 76 body, err := json.Marshal(response) |
60 if err != nil { | 77 if err != nil { |
61 return err | 78 return err |
62 } | 79 } |
63 w.Write(body) | 80 w.Write(body) |
64 return nil | 81 return nil |
65 } | 82 } |
66 | 83 |
| 84 // sendFilesList sends a JSON-encoded response to the client including the list |
| 85 // of files contained in the given path. |
| 86 func (h *charmsHandler) sendFilesList(w http.ResponseWriter, path string) { |
| 87 var files []string |
| 88 err := filepath.Walk(path, func(filePath string, fileInfo os.FileInfo, e
rr error) error { |
| 89 if err != nil { |
| 90 return err |
| 91 } |
| 92 // Exclude directories. |
| 93 if fileInfo.IsDir() { |
| 94 return nil |
| 95 } |
| 96 relPath, err := filepath.Rel(path, filePath) |
| 97 if err != nil { |
| 98 return err |
| 99 } |
| 100 files = append(files, relPath) |
| 101 return nil |
| 102 }) |
| 103 if err != nil { |
| 104 http.Error( |
| 105 w, fmt.Sprintf("unable to list files in %q: %v", path, e
rr), |
| 106 http.StatusInternalServerError) |
| 107 return |
| 108 } |
| 109 h.sendJSON(w, http.StatusOK, ¶ms.CharmsResponse{Files: files}) |
| 110 } |
| 111 |
| 112 // sendFile sends the file contents of the file present in the given path. |
| 113 // A 404 page not found is returned if path does not exist. |
| 114 // A 403 forbidden error is returned if path points to a directory. |
| 115 func (h *charmsHandler) sendFile(w http.ResponseWriter, r *http.Request, path st
ring) { |
| 116 file, err := os.Open(path) |
| 117 if os.IsNotExist(err) { |
| 118 http.NotFound(w, r) |
| 119 return |
| 120 } else if err != nil { |
| 121 http.Error( |
| 122 w, fmt.Sprintf("unable to open file at %q: %v", path, er
r), |
| 123 http.StatusInternalServerError) |
| 124 return |
| 125 } |
| 126 defer file.Close() |
| 127 fileInfo, err := file.Stat() |
| 128 if err != nil { |
| 129 http.Error( |
| 130 w, fmt.Sprintf("unable to get info about %q: %v", path,
err), |
| 131 http.StatusInternalServerError) |
| 132 return |
| 133 } |
| 134 // Deny directory listing. |
| 135 if fileInfo.IsDir() { |
| 136 http.Error(w, "directory listing not allowed", http.StatusForbid
den) |
| 137 return |
| 138 } |
| 139 http.ServeContent(w, r, fileInfo.Name(), fileInfo.ModTime(), file) |
| 140 } |
| 141 |
67 // sendError sends a JSON-encoded error response. | 142 // sendError sends a JSON-encoded error response. |
68 func (h *charmsHandler) sendError(w http.ResponseWriter, statusCode int, message
string) error { | 143 func (h *charmsHandler) sendError(w http.ResponseWriter, statusCode int, message
string) error { |
69 return h.sendJSON(w, statusCode, ¶ms.CharmsResponse{Error: message}) | 144 return h.sendJSON(w, statusCode, ¶ms.CharmsResponse{Error: message}) |
70 } | 145 } |
71 | 146 |
72 // authenticate parses HTTP basic authentication and authorizes the | 147 // authenticate parses HTTP basic authentication and authorizes the |
73 // request by looking up the provided tag and password against state. | 148 // request by looking up the provided tag and password against state. |
74 func (h *charmsHandler) authenticate(r *http.Request) error { | 149 func (h *charmsHandler) authenticate(r *http.Request) error { |
75 parts := strings.Fields(r.Header.Get("Authorization")) | 150 parts := strings.Fields(r.Header.Get("Authorization")) |
76 if len(parts) != 2 || parts[0] != "Basic" { | 151 if len(parts) != 2 || parts[0] != "Basic" { |
(...skipping 29 matching lines...) Expand all Loading... |
106 func (h *charmsHandler) authError(w http.ResponseWriter) { | 181 func (h *charmsHandler) authError(w http.ResponseWriter) { |
107 w.Header().Set("WWW-Authenticate", `Basic realm="juju"`) | 182 w.Header().Set("WWW-Authenticate", `Basic realm="juju"`) |
108 h.sendError(w, http.StatusUnauthorized, "unauthorized") | 183 h.sendError(w, http.StatusUnauthorized, "unauthorized") |
109 } | 184 } |
110 | 185 |
111 // processPost handles a charm upload POST request after authentication. | 186 // processPost handles a charm upload POST request after authentication. |
112 func (h *charmsHandler) processPost(r *http.Request) (*charm.URL, error) { | 187 func (h *charmsHandler) processPost(r *http.Request) (*charm.URL, error) { |
113 query := r.URL.Query() | 188 query := r.URL.Query() |
114 series := query.Get("series") | 189 series := query.Get("series") |
115 if series == "" { | 190 if series == "" { |
116 » » return nil, fmt.Errorf("expected series= URL argument") | 191 » » return nil, fmt.Errorf("expected series=URL argument") |
117 } | 192 } |
118 // Make sure the content type is zip. | 193 // Make sure the content type is zip. |
119 contentType := r.Header.Get("Content-Type") | 194 contentType := r.Header.Get("Content-Type") |
120 if contentType != "application/zip" { | 195 if contentType != "application/zip" { |
121 return nil, fmt.Errorf("expected Content-Type: application/zip,
got: %v", contentType) | 196 return nil, fmt.Errorf("expected Content-Type: application/zip,
got: %v", contentType) |
122 } | 197 } |
123 tempFile, err := ioutil.TempFile("", "charm") | 198 tempFile, err := ioutil.TempFile("", "charm") |
124 if err != nil { | 199 if err != nil { |
125 return nil, fmt.Errorf("cannot create temp file: %v", err) | 200 return nil, fmt.Errorf("cannot create temp file: %v", err) |
126 } | 201 } |
(...skipping 285 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
412 return errgo.Annotate(err, "cannot parse storage URL") | 487 return errgo.Annotate(err, "cannot parse storage URL") |
413 } | 488 } |
414 | 489 |
415 // And finally, update state. | 490 // And finally, update state. |
416 _, err = h.state.UpdateUploadedCharm(archive, curl, bundleURL, bundleSHA
256) | 491 _, err = h.state.UpdateUploadedCharm(archive, curl, bundleURL, bundleSHA
256) |
417 if err != nil { | 492 if err != nil { |
418 return errgo.Annotate(err, "cannot update uploaded charm in stat
e") | 493 return errgo.Annotate(err, "cannot update uploaded charm in stat
e") |
419 } | 494 } |
420 return nil | 495 return nil |
421 } | 496 } |
| 497 |
| 498 // processGet handles a charm file download GET request after authentication. |
| 499 // It returns the bundle path, the requested file path (if any) and an error. |
| 500 func (h *charmsHandler) processGet(r *http.Request) (string, string, error) { |
| 501 query := r.URL.Query() |
| 502 |
| 503 // Retrieve and validate query parameters. |
| 504 curl := query.Get("url") |
| 505 if curl == "" { |
| 506 return "", "", fmt.Errorf("expected url=CharmURL query argument"
) |
| 507 } |
| 508 file := query.Get("file") |
| 509 |
| 510 // Prepare the bundle directories. |
| 511 name := charm.Quote(curl) |
| 512 bundlePath := filepath.Join(h.dataDir, "charm-get-cache", name) |
| 513 filePath, err := h.getFilePath(bundlePath, file) |
| 514 if err != nil { |
| 515 return "", "", err |
| 516 } |
| 517 |
| 518 // Check if the charm bundle is already in the cache. |
| 519 if _, err := os.Stat(bundlePath); os.IsNotExist(err) { |
| 520 // Download the charm and extract the bundle. |
| 521 if err = h.downloadCharm(name, bundlePath); err != nil { |
| 522 return "", "", fmt.Errorf("unable to retrieve and save t
he charm: %v", err) |
| 523 } |
| 524 } else if err != nil { |
| 525 return "", "", fmt.Errorf("cannot access the charms cache: %v",
err) |
| 526 } |
| 527 return bundlePath, filePath, nil |
| 528 } |
| 529 |
| 530 // getFilePath return the absolute path of a charm file, based on the given |
| 531 // bundlePath. It also checks that the resulting path lives inside the bundle. |
| 532 func (h *charmsHandler) getFilePath(bundlePath, file string) (string, error) { |
| 533 if file == "" { |
| 534 return "", nil |
| 535 } |
| 536 filePath, err := filepath.Abs(filepath.Join(bundlePath, file)) |
| 537 if err != nil { |
| 538 return "", errgo.Annotate(err, "cannot retrieve the requested pa
th") |
| 539 } |
| 540 if !strings.HasPrefix(filePath, bundlePath+"/") { |
| 541 return "", fmt.Errorf("invalid file path: %q", file) |
| 542 } |
| 543 return filePath, nil |
| 544 } |
| 545 |
| 546 // downloadCharm downloads the given charm name from the provider storage and |
| 547 // extracts the corresponding bundle to the given bundlePath. |
| 548 func (h *charmsHandler) downloadCharm(name, bundlePath string) error { |
| 549 // Get the provider storage. |
| 550 storage, err := envtesting.GetEnvironStorage(h.state) |
| 551 if err != nil { |
| 552 return errgo.Annotate(err, "cannot access provider storage") |
| 553 } |
| 554 |
| 555 // Use the storage to retrieve and save the charm archive. |
| 556 reader, err := storage.Get(name) |
| 557 if err != nil { |
| 558 return errgo.Annotate(err, "charm not found in the provider stor
age") |
| 559 } |
| 560 defer reader.Close() |
| 561 data, err := ioutil.ReadAll(reader) |
| 562 if err != nil { |
| 563 return errgo.Annotate(err, "cannot read charm data") |
| 564 } |
| 565 tempCharm, err := ioutil.TempFile("", "charm") |
| 566 if err != nil { |
| 567 return errgo.Annotate(err, "cannot create charm archive temp fil
e") |
| 568 } |
| 569 defer tempCharm.Close() |
| 570 defer os.Remove(tempCharm.Name()) |
| 571 if err = ioutil.WriteFile(tempCharm.Name(), data, 0644); err != nil { |
| 572 return errgo.Annotate(err, "error processing charm archive downl
oad") |
| 573 } |
| 574 |
| 575 // Read and expand the charm bundle. |
| 576 bundle, err := charm.ReadBundle(tempCharm.Name()) |
| 577 if err != nil { |
| 578 return errgo.Annotate(err, "cannot read the charm bundle") |
| 579 } |
| 580 // In order to avoid races, the bundle is expanded in a temporary dir wh
ich |
| 581 // is then atomically renamed. The temporary directory is created in the |
| 582 // charm cache so that we can safely assume the rename source and target |
| 583 // live in the same file system. |
| 584 cacheDir, _ := filepath.Split(bundlePath) |
| 585 if err = os.MkdirAll(cacheDir, 0755); err != nil { |
| 586 return errgo.Annotate(err, "cannot create the charms cache") |
| 587 } |
| 588 bundleTempPath, err := ioutil.TempDir(cacheDir, "bundle") |
| 589 if err != nil { |
| 590 return errgo.Annotate(err, "cannot create the temporary bundle d
irectory") |
| 591 } |
| 592 if err = bundle.ExpandTo(bundleTempPath); err != nil { |
| 593 defer os.RemoveAll(bundleTempPath) |
| 594 return errgo.Annotate(err, "error expanding the bundle") |
| 595 } |
| 596 if err = os.Rename(bundleTempPath, bundlePath); err != nil { |
| 597 return errgo.Annotate(err, "error renaming the bundle") |
| 598 } |
| 599 return nil |
| 600 } |
OLD | NEW |