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 19, 2024
1 parent 35d6b58 commit 866929a
Showing 1 changed file with 97 additions and 12 deletions.
109 changes: 97 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,101 @@ 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, and only GCS is supported
// for local development
err := validateGcsSignedUpload(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 validateGcsSignedUpload(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.

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 866929a

Please sign in to comment.