Skip to content

Commit

Permalink
Add a PUT handler for the local bucket server.
Browse files Browse the repository at this point in the history
  • Loading branch information
erikcarlsson committed Dec 16, 2024
1 parent 7aca2b4 commit 5652749
Showing 1 changed file with 103 additions and 12 deletions.
115 changes: 103 additions & 12 deletions cli/daemon/objects/public.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package objects

import (
"errors"
"fmt"
"io"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"

"encr.dev/pkg/emulators/storage/gcsemu"
"google.golang.org/api/storage/v1"
)

// Fallback is a function that returns a store for a given namespace.
Expand Down Expand Up @@ -77,22 +83,107 @@ func (s *PublicBucketServer) handler(w http.ResponseWriter, req *http.Request) {
http.Error(w, "unknown namespace", http.StatusNotFound)
return
}
switch req.Method {
case "GET":
obj, contents, err := store.Get("", bucketName, objName)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} else if obj == nil {
http.Error(w, "object not found", http.StatusNotFound)
return
}

obj, contents, err := store.Get("", bucketName, objName)
if obj.ContentType != "" {
w.Header().Set("Content-Type", obj.ContentType)
}
if obj.Etag != "" {
w.Header().Set("Etag", obj.Etag)
}
w.Header().Set("Content-Length", strconv.Itoa(len(contents)))
w.Write(contents)
case "PUT":
// Only signed URLs are supported for PUT.
err := validateSignedUpload(req, time.Now())
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

buf, err := io.ReadAll(req.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

metaIn := parseObjectMeta(req)
err = store.Add(bucketName, objName, buf, &metaIn)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// Read back the object so we can add the etag value to the response.
metaOut, _, err := store.Get("", bucketName, objName)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Etag", metaOut.Etag)
default:
http.Error(w, "method not allowed", http.StatusBadRequest)
}
}

func validateSignedUpload(req *http.Request, now time.Time) error {
const dateLayout = "20060102T150405Z"
const gracePeriod = time.Duration(30) * time.Second

query := map[string]string{}
for k, vs := range req.URL.Query() {
query[strings.ToLower(k)] = vs[0]
}

// We don't try to actually verify the signature, we only check that it's non-empty.

// This URL parsing is specific to GCS URLs. OTOH, we would like this
// bucket server to look as generic as possible to apps running locally.
// To avoid apps accidentally implementing provider specific behaviour
// that fails if deployed on a different cloud in prod. It might seem like
// the "x-goog-" fields below contradicts this. However: 1) the upload URLs
// are (supposed to be) opaque to clients, and 2) we always use the GCS URL
// signer in local mode.
for _, s := range []string{
"x-goog-signature",
"x-goog-credential",
"x-goog-date",
"x-goog-expires"} {
if len(query[s]) <= 0 {
return fmt.Errorf("missing or empty query param %q", s)
}
}

t0, err := time.Parse(dateLayout, query["x-goog-date"])
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} else if obj == nil {
http.Error(w, "object not found", http.StatusNotFound)
return
return errors.New("failed to parse x-goog-date")
}
if t0.After(now.Add(gracePeriod)) {
return errors.New("URL expiration base date is in the future")
}

if obj.ContentType != "" {
w.Header().Set("Content-Type", obj.ContentType)
td, err := strconv.Atoi(query["x-goog-expires"])
if err != nil {
return errors.New("failed to parse x-goog-expires value into an integer")
}
if obj.Etag != "" {
w.Header().Set("Etag", obj.Etag)
t := t0.Add(time.Duration(td) * time.Second)

if t.Before(now.Add(-gracePeriod)) {
return errors.New("URL is expired")
}
w.Header().Set("Content-Length", strconv.Itoa(len(contents)))
w.Write(contents)

return nil
}

func parseObjectMeta(req *http.Request) storage.Object {
return storage.Object{ContentType: req.Header.Get("Content-Type")}
}

0 comments on commit 5652749

Please sign in to comment.