diff --git a/server/go/blobserver/camlistored.go b/server/go/blobserver/camlistored.go index bf1841e0a..813e35767 100644 --- a/server/go/blobserver/camlistored.go +++ b/server/go/blobserver/camlistored.go @@ -32,7 +32,7 @@ func handleCamli(conn http.ResponseWriter, req *http.Request) { case "/camli/enumerate-blobs": handler = auth.RequireAuth(handleEnumerateBlobs) default: - handler = auth.RequireAuth(createGetHandler(blobFetcher)) + handler = createGetHandler(blobFetcher) } case "POST": switch req.URL.Path { diff --git a/server/go/blobserver/get.go b/server/go/blobserver/get.go index 507c70f10..9196a98e7 100644 --- a/server/go/blobserver/get.go +++ b/server/go/blobserver/get.go @@ -1,26 +1,106 @@ package main import ( + "camli/auth" "camli/blobref" "camli/httputil" "fmt" "http" "os" "io" + "json" + "log" + "strings" + "time" ) -func createGetHandler(fetcher blobref.Fetcher) func (http.ResponseWriter, *http.Request) { - return func (conn http.ResponseWriter, req *http.Request) { - handleGet(conn, req, fetcher); +func createGetHandler(fetcher blobref.Fetcher) func(http.ResponseWriter, *http.Request) { + return func(conn http.ResponseWriter, req *http.Request) { + handleGet(conn, req, fetcher) } } +const fetchFailureDelayNs = 200e6 // 200 ms +const maxJsonSize = 10 * 1024 + func handleGet(conn http.ResponseWriter, req *http.Request, fetcher blobref.Fetcher) { + isOwner := auth.IsAuthorized(req) + blobRef := BlobFromUrlPath(req.URL.Path) if blobRef == nil { httputil.BadRequestError(conn, "Malformed GET URL.") return } + + var viaBlobs []*blobref.BlobRef + if !isOwner { + viaPathOkay := false + startTime := time.Nanoseconds() + defer func() { + if !viaPathOkay { + // Insert a delay, to hide timing attacks probing + // for the existence of blobs. + sleep := fetchFailureDelayNs - (time.Nanoseconds() - startTime) + if sleep > 0 { + time.Sleep(sleep) + } + } + }() + viaBlobs = make([]*blobref.BlobRef, 0) + if via := req.FormValue("via"); via != "" { + for _, vs := range strings.Split("via", ",", -1) { + if br := blobref.Parse(vs); br == nil { + httputil.BadRequestError(conn, "Malformed blobref in via param") + return + } else { + viaBlobs = append(viaBlobs, br) + } + } + } + + fetchChain := make([]*blobref.BlobRef, 0) + fetchChain = append(fetchChain, viaBlobs...) + fetchChain = append(fetchChain, blobRef) + for i, br := range fetchChain { + switch i { + case 0: + file, size, err := fetcher.Fetch(br) + if err != nil { + log.Printf("Fetch chain 0 of %s failed: %v", br.String(), err) + conn.WriteHeader(http.StatusUnauthorized) + return + } + defer file.Close() + if size > maxJsonSize { + log.Printf("Fetch chain 0 of %s too large", br.String()) + conn.WriteHeader(http.StatusUnauthorized) + return + } + jd := json.NewDecoder(file) + m := make(map[string]interface{}) + if err := jd.Decode(&m); err != nil { + log.Printf("Fetch chain 0 of %s wasn't JSON: %v", br.String(), err) + conn.WriteHeader(http.StatusUnauthorized) + return + } + if m["camliType"].(string) != "share" { + log.Printf("Fetch chain 0 of %s wasn't a share", br.String()) + conn.WriteHeader(http.StatusUnauthorized) + return + } + if len(fetchChain) > 1 && fetchChain[1].String() != m["target"].(string) { + log.Printf("Fetch chain 0->1 (%s -> %q) unauthorized, expected hop to %q", + br.String(), fetchChain[1].String(), m["target"]) + conn.WriteHeader(http.StatusUnauthorized) + return + } + default: + log.Printf("TODO: FETCH %s", br.String()) + } + } + viaPathOkay = true + } + file, size, err := fetcher.Fetch(blobRef) switch err { case nil: @@ -60,8 +140,8 @@ func handleGet(conn http.ResponseWriter, req *http.Request, fetcher blobref.Fetc if !reqRange.IsWholeFile() { conn.SetHeader("Content-Range", fmt.Sprintf("bytes %d-%d/%d", reqRange.SkipBytes, - reqRange.SkipBytes + remainBytes, - size)) + reqRange.SkipBytes+remainBytes, + size)) conn.WriteHeader(http.StatusPartialContent) } bytesCopied, err := io.Copy(conn, input)