Example #1
0
func (h *Handler) handleVerify(rw http.ResponseWriter, req *http.Request) {
	req.ParseForm()
	sjson := req.FormValue("sjson")
	if sjson == "" {
		http.Error(rw, "missing \"sjson\" parameter", http.StatusBadRequest)
		return
	}

	// TODO: use a different fetcher here that checks memory, disk,
	// the internet, etc.
	fetcher := h.pubKeyFetcher

	var res camtypes.VerifyResponse
	vreq := jsonsign.NewVerificationRequest(sjson, fetcher)
	if vreq.Verify() {
		res.SignatureValid = true
		res.SignerKeyId = vreq.SignerKeyId
		res.VerifiedData = vreq.PayloadMap
	} else {
		res.SignatureValid = false
		res.ErrorMessage = vreq.Err.Error()
	}

	rw.WriteHeader(http.StatusOK) // no HTTP response code fun, error info in JSON
	httputil.ReturnJSON(rw, &res)
}
Example #2
0
func (a *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path == a.masterqueryURLPath {
		a.handleMasterQuery(w, r)
		return
	}
	if a.configURLPath != "" && r.URL.Path == a.configURLPath {
		if a.auth.AllowedAccess(r)&auth.OpGet == auth.OpGet {
			camhttputil.ReturnJSON(w, a.appConfig)
		} else {
			auth.SendUnauthorized(w, r)
		}
		return
	}
	trimmedPath := strings.TrimPrefix(r.URL.Path, a.prefix)
	if strings.HasPrefix(trimmedPath, "/search") {
		a.handleSearch(w, r)
		return
	}

	if a.proxy == nil {
		http.Error(w, "no proxy for the app", 500)
		return
	}
	a.proxy.ServeHTTP(w, r)
}
Example #3
0
func (sh *Handler) serveFiles(rw http.ResponseWriter, req *http.Request) {
	var ret camtypes.FileSearchResponse
	defer httputil.ReturnJSON(rw, &ret)

	br, ok := blob.Parse(req.FormValue("wholedigest"))
	if !ok {
		ret.Error = "Missing or invalid 'wholedigest' param"
		ret.ErrorType = "input"
		return
	}

	files, err := sh.index.ExistingFileSchemas(br)
	if err != nil {
		ret.Error = err.Error()
		ret.ErrorType = "server"
		return
	}

	// the ui code expects an object
	if files == nil {
		files = []blob.Ref{}
	}

	ret.Files = files
	return
}
Example #4
0
func (h *Handler) handleVerify(rw http.ResponseWriter, req *http.Request) {
	req.ParseForm()
	sjson := req.FormValue("sjson")
	if sjson == "" {
		http.Error(rw, "missing \"sjson\" parameter", http.StatusBadRequest)
		return
	}

	m := make(map[string]interface{})

	// TODO: use a different fetcher here that checks memory, disk,
	// the internet, etc.
	fetcher := h.pubKeyFetcher

	vreq := jsonsign.NewVerificationRequest(sjson, fetcher)
	if vreq.Verify() {
		m["signatureValid"] = 1
		m["signerKeyId"] = vreq.SignerKeyId
		m["verifiedData"] = vreq.PayloadMap
	} else {
		errStr := vreq.Err.Error()
		m["signatureValid"] = 0
		m["errorMessage"] = errStr
	}

	rw.WriteHeader(http.StatusOK) // no HTTP response code fun, error info in JSON
	httputil.ReturnJSON(rw, m)
}
Example #5
0
func handleVerify(conn http.ResponseWriter, req *http.Request) {
	if !(req.Method == "POST" && req.URL.Path == "/camli/sig/verify") {
		httputil.BadRequestError(conn, "Inconfigured handler.")
		return
	}

	req.ParseForm()
	sjson := req.FormValue("sjson")
	if sjson == "" {
		httputil.BadRequestError(conn, "Missing sjson parameter.")
		return
	}

	m := make(map[string]interface{})

	vreq := jsonsign.NewVerificationRequest(sjson, pubKeyFetcher)
	if vreq.Verify() {
		m["signatureValid"] = 1
		m["verifiedData"] = vreq.PayloadMap
	} else {
		m["signatureValid"] = 0
		m["errorMessage"] = vreq.Err.Error()
	}

	conn.WriteHeader(http.StatusOK) // no HTTP response code fun, error info in JSON
	httputil.ReturnJSON(conn, m)
}
Example #6
0
func (sh *StatusHandler) serveStatus(rw http.ResponseWriter, req *http.Request) {
	res := &statusResponse{
		Version: buildinfo.Version(),
	}

	httputil.ReturnJSON(rw, res)
}
Example #7
0
func (sh *Handler) serveSignerAttrValue(rw http.ResponseWriter, req *http.Request) {
	defer httputil.RecoverJSON(rw, req)
	signer := httputil.MustGetBlobRef(req, "signer")
	attr := httputil.MustGet(req, "attr")
	value := httputil.MustGet(req, "value")

	pn, err := sh.index.PermanodeOfSignerAttrValue(signer, attr, value)
	if err != nil {
		httputil.ServeJSONError(rw, err)
		return
	}

	dr := sh.NewDescribeRequest()
	dr.Describe(pn, 2)
	metaMap, err := dr.metaMap()
	if err != nil {
		httputil.ServeJSONError(rw, err)
		return
	}

	httputil.ReturnJSON(rw, &SignerAttrValueResponse{
		Permanode: pn,
		Meta:      metaMap,
	})
}
Example #8
0
func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	base := req.Header.Get("X-PrefixHandler-PathBase")
	subPath := req.Header.Get("X-PrefixHandler-PathSuffix")
	switch req.Method {
	case "GET":
		switch subPath {
		case "":
			http.Redirect(rw, req, base+"camli/sig/discovery", http.StatusFound)
			return
		case h.pubKeyBlobRefServeSuffix:
			h.pubKeyHandler.ServeHTTP(rw, req)
			return
		case "camli/sig/sign":
			fallthrough
		case "camli/sig/verify":
			http.Error(rw, "POST required", 400)
			return
		case "camli/sig/discovery":
			httputil.ReturnJSON(rw, h.DiscoveryMap(base))
			return
		}
	case "POST":
		switch subPath {
		case "camli/sig/sign":
			h.handleSign(rw, req)
			return
		case "camli/sig/verify":
			h.handleVerify(rw, req)
			return
		}
	}
	http.Error(rw, "Unsupported path or method.", http.StatusBadRequest)
}
Example #9
0
func (sh *Handler) serveFiles(rw http.ResponseWriter, req *http.Request) {
	ret := jsonMap()
	defer httputil.ReturnJSON(rw, ret)

	br, ok := blob.Parse(req.FormValue("wholedigest"))
	if !ok {
		ret["error"] = "Missing or invalid 'wholedigest' param"
		ret["errorType"] = "input"
		return
	}

	files, err := sh.index.ExistingFileSchemas(br)
	if err != nil {
		ret["error"] = err.Error()
		ret["errorType"] = "server"
		return
	}

	strList := []string{}
	for _, br := range files {
		strList = append(strList, br.String())
	}
	ret["files"] = strList
	return
}
Example #10
0
func (sh *Handler) serveSignerPaths(rw http.ResponseWriter, req *http.Request) {
	ret := jsonMap()
	defer httputil.ReturnJSON(rw, ret)
	defer setPanicError(ret)

	signer := blobref.MustParse(mustGet(req, "signer"))
	target := blobref.MustParse(mustGet(req, "target"))
	paths, err := sh.index.PathsOfSignerTarget(signer, target)
	if err != nil {
		ret["error"] = err.Error()
	} else {
		jpaths := []map[string]interface{}{}
		for _, path := range paths {
			jpaths = append(jpaths, map[string]interface{}{
				"claimRef": path.Claim.String(),
				"baseRef":  path.Base.String(),
				"suffix":   path.Suffix,
			})
		}
		ret["paths"] = jpaths
		dr := sh.NewDescribeRequest()
		for _, path := range paths {
			dr.Describe(path.Base, 2)
		}
		dr.PopulateJSON(ret)
	}
}
Example #11
0
func (fth *FileTreeHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	if req.Method != "GET" && req.Method != "HEAD" {
		http.Error(rw, "Invalid method", 400)
		return
	}
	ret := make(map[string]interface{})
	defer httputil.ReturnJSON(rw, ret)

	de, err := schema.NewDirectoryEntryFromBlobRef(fth.storageSeekFetcher(), fth.file)
	dir, err := de.Directory()
	if err != nil {
		http.Error(rw, "Error reading directory", 500)
		log.Printf("Error reading directory from blobref %s: %v\n", fth.file, err)
		return
	}
	entries, err := dir.Readdir(-1)
	if err != nil {
		http.Error(rw, "Error reading directory", 500)
		log.Printf("reading dir from blobref %s: %v\n", fth.file, err)
		return
	}
	children := make([]map[string]interface{}, 0)
	for _, v := range entries {
		child := map[string]interface{}{
			"name":    v.FileName(),
			"type":    v.CamliType(),
			"blobRef": v.BlobRef(),
		}
		children = append(children, child)
	}
	ret["children"] = children
}
Example #12
0
func (sh *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	suffix := httputil.PathSuffix(req)

	handlers := getHandler
	switch {
	case httputil.IsGet(req):
		// use default from above
	case req.Method == "POST":
		handlers = postHandler
	default:
		handlers = nil
	}
	fn := handlers[strings.TrimPrefix(suffix, "camli/search/")]
	if fn != nil {
		fn(sh, rw, req)
		return
	}

	// TODO: discovery for the endpoints & better error message with link to discovery info
	ret := camtypes.SearchErrorResponse{
		Error:     "Unsupported search path or method",
		ErrorType: "input",
	}
	httputil.ReturnJSON(rw, &ret)
}
Example #13
0
func handleRemove(conn http.ResponseWriter, req *http.Request, storage blobserver.Storage) {
	if w, ok := storage.(blobserver.ContextWrapper); ok {
		storage = w.WrapContext(req)
	}

	if req.Method != "POST" {
		log.Fatalf("Invalid method; handlers misconfigured")
	}

	configer, ok := storage.(blobserver.Configer)
	if !ok {
		conn.WriteHeader(http.StatusForbidden)
		fmt.Fprintf(conn, "Remove handler's blobserver.Storage isn't a blobserver.Configer; can't remove")
		return
	}
	if !configer.Config().IsQueue {
		conn.WriteHeader(http.StatusForbidden)
		fmt.Fprintf(conn, "Can only remove blobs from a queue.\n")
		return
	}

	n := 0
	toRemove := make([]*blobref.BlobRef, 0)
	toRemoveStr := make([]string, 0)
	for {
		n++
		if n > maxRemovesPerRequest {
			httputil.BadRequestError(conn,
				fmt.Sprintf("Too many removes in this request; max is %d", maxRemovesPerRequest))
			return
		}
		key := fmt.Sprintf("blob%v", n)
		value := req.FormValue(key)
		if value == "" {
			break
		}
		ref := blobref.Parse(value)
		if ref == nil {
			httputil.BadRequestError(conn, "Bogus blobref for key "+key)
			return
		}
		toRemove = append(toRemove, ref)
		toRemoveStr = append(toRemoveStr, ref.String())
	}

	err := storage.RemoveBlobs(toRemove)
	if err != nil {
		conn.WriteHeader(http.StatusInternalServerError)
		log.Printf("Server error during remove: %v", err)
		fmt.Fprintf(conn, "Server error")
		return
	}

	reply := make(map[string]interface{}, 0)
	reply["removed"] = toRemoveStr
	httputil.ReturnJSON(conn, reply)
}
Example #14
0
func (sh *Handler) serveRecentPermanodes(rw http.ResponseWriter, req *http.Request) {
	defer httputil.RecoverJSON(rw, req)
	var rr RecentRequest
	rr.fromHTTP(req)
	res, err := sh.GetRecentPermanodes(&rr)
	if err != nil {
		httputil.ServeJSONError(rw, err)
		return
	}
	httputil.ReturnJSON(rw, res)
}
Example #15
0
// servePermanodesWithAttr uses the indexer to search for the permanodes matching
// the request.
// The valid values for the "attr" key in the request (i.e the only attributes
// for a permanode which are actually indexed as such) are "tag" and "title".
func (sh *Handler) servePermanodesWithAttr(rw http.ResponseWriter, req *http.Request) {
	defer httputil.RecoverJSON(rw, req)
	var wr WithAttrRequest
	wr.fromHTTP(req)
	res, err := sh.GetPermanodesWithAttr(&wr)
	if err != nil {
		httputil.ServeJSONError(rw, err)
		return
	}
	httputil.ReturnJSON(rw, res)
}
Example #16
0
func (sh *Handler) serveClaims(rw http.ResponseWriter, req *http.Request) {
	defer httputil.RecoverJSON(rw, req)
	var cr ClaimsRequest
	cr.fromHTTP(req)
	res, err := sh.GetClaims(&cr)
	if err != nil {
		httputil.ServeJSONError(rw, err)
		return
	}
	httputil.ReturnJSON(rw, res)
}
Example #17
0
// Unlike the index interface's EdgesTo method, the "edgesto" Handler
// here additionally filters out since-deleted permanode edges.
func (sh *Handler) serveEdgesTo(rw http.ResponseWriter, req *http.Request) {
	defer httputil.RecoverJSON(rw, req)
	var er EdgesRequest
	er.fromHTTP(req)
	res, err := sh.EdgesTo(&er)
	if err != nil {
		httputil.ServeJSONError(rw, err)
		return
	}
	httputil.ReturnJSON(rw, res)
}
Example #18
0
func (sh *Handler) serveDescribe(rw http.ResponseWriter, req *http.Request) {
	defer httputil.RecoverJSON(rw, req)
	var dr DescribeRequest
	dr.fromHTTP(req)

	res, err := sh.Describe(&dr)
	if err != nil {
		httputil.ServeJSONError(rw, err)
		return
	}
	httputil.ReturnJSON(rw, res)
}
Example #19
0
func (sh *Handler) serveSignerPaths(rw http.ResponseWriter, req *http.Request) {
	defer httputil.RecoverJSON(rw, req)
	var sr SignerPathsRequest
	sr.fromHTTP(req)

	res, err := sh.GetSignerPaths(&sr)
	if err != nil {
		httputil.ServeJSONError(rw, err)
		return
	}
	httputil.ReturnJSON(rw, res)
}
Example #20
0
func (sh *Handler) serveDescribe(rw http.ResponseWriter, req *http.Request) {
	defer httputil.RecoverJSON(rw, req)
	br := httputil.MustGetBlobRef(req, "blobref")

	dr := sh.NewDescribeRequest()
	dr.Describe(br, 4)
	metaMap, err := dr.metaMapThumbs(thumbnailSize(req))
	if err != nil {
		httputil.ServeJSONError(rw, err)
		return
	}
	httputil.ReturnJSON(rw, &DescribeResponse{metaMap})
}
Example #21
0
func (ui *UIHandler) serveUploadHelper(rw http.ResponseWriter, req *http.Request) {
	if ui.root.Storage == nil {
		httputil.ServeJSONError(rw, httputil.ServerError("No BlobRoot configured"))
		return
	}

	mr, err := req.MultipartReader()
	if err != nil {
		httputil.ServeJSONError(rw, httputil.ServerError("reading body: "+err.Error()))
		return
	}

	var got []*uploadHelperGotItem
	var modTime types.Time3339
	for {
		part, err := mr.NextPart()
		if err == io.EOF {
			break
		}
		if err != nil {
			httputil.ServeJSONError(rw, httputil.ServerError("reading body: "+err.Error()))
			break
		}
		if part.FormName() == "modtime" {
			payload, err := ioutil.ReadAll(part)
			if err != nil {
				log.Printf("ui uploadhelper: unable to read part for modtime: %v", err)
				continue
			}
			modTime = types.ParseTime3339OrZero(string(payload))
			continue
		}
		fileName := part.FileName()
		if fileName == "" {
			continue
		}
		br, err := schema.WriteFileFromReaderWithModTime(ui.root.Storage, fileName, modTime.Time(), part)
		if err != nil {
			httputil.ServeJSONError(rw, httputil.ServerError("writing to blobserver: "+err.Error()))
			return
		}
		got = append(got, &uploadHelperGotItem{
			FileName: part.FileName(),
			ModTime:  modTime,
			FormName: part.FormName(),
			FileRef:  br,
		})
	}

	httputil.ReturnJSON(rw, &uploadHelperResponse{Got: got})
}
Example #22
0
func handleRemove(rw http.ResponseWriter, req *http.Request, storage blobserver.Storage) {
	if req.Method != "POST" {
		log.Fatalf("Invalid method; handlers misconfigured")
	}

	configer, ok := storage.(blobserver.Configer)
	if !ok {
		rw.WriteHeader(http.StatusForbidden)
		fmt.Fprintf(rw, "Remove handler's blobserver.Storage isn't a blobserver.Configer; can't remove")
		return
	}
	if !configer.Config().Deletable {
		rw.WriteHeader(http.StatusForbidden)
		fmt.Fprintf(rw, "storage does not permit deletes.\n")
		return
	}

	n := 0
	toRemove := make([]blob.Ref, 0)
	for {
		n++
		if n > maxRemovesPerRequest {
			httputil.BadRequestError(rw,
				fmt.Sprintf("Too many removes in this request; max is %d", maxRemovesPerRequest))
			return
		}
		key := fmt.Sprintf("blob%v", n)
		value := req.FormValue(key)
		if value == "" {
			break
		}
		ref, ok := blob.Parse(value)
		if !ok {
			httputil.BadRequestError(rw, "Bogus blobref for key "+key)
			return
		}
		toRemove = append(toRemove, ref)
	}

	err := storage.RemoveBlobs(toRemove)
	if err != nil {
		rw.WriteHeader(http.StatusInternalServerError)
		log.Printf("Server error during remove: %v", err)
		fmt.Fprintf(rw, "Server error")
		return
	}

	httputil.ReturnJSON(rw, &RemoveResponse{Removed: toRemove})
}
Example #23
0
func (a *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	if camhttputil.PathSuffix(req) == "config.json" {
		if a.auth.AllowedAccess(req)&auth.OpGet == auth.OpGet {
			camhttputil.ReturnJSON(rw, a.appConfig)
		} else {
			auth.SendUnauthorized(rw, req)
		}
		return
	}
	if a.proxy == nil {
		http.Error(rw, "no proxy for the app", 500)
		return
	}
	a.proxy.ServeHTTP(rw, req)
}
Example #24
0
func (ui *UIHandler) serveUploadHelper(rw http.ResponseWriter, req *http.Request) {
	ret := make(map[string]interface{})
	defer httputil.ReturnJSON(rw, ret)

	if ui.root.Storage == nil {
		ret["error"] = "No BlobRoot configured"
		ret["errorType"] = "server"
		return
	}

	mr, err := req.MultipartReader()
	if err != nil {
		ret["error"] = "reading body: " + err.Error()
		ret["errorType"] = "server"
		return
	}

	got := make([]map[string]interface{}, 0)
	for {
		part, err := mr.NextPart()
		if err == io.EOF {
			break
		}
		if err != nil {
			ret["error"] = "reading body: " + err.Error()
			ret["errorType"] = "server"
			break
		}
		fileName := part.FileName()
		if fileName == "" {
			continue
		}
		br, err := schema.WriteFileFromReader(ui.root.Storage, fileName, part)

		if err == nil {
			got = append(got, map[string]interface{}{
				"filename": part.FileName(),
				"formname": part.FormName(),
				"fileref":  br.String(),
			})
		} else {
			ret["error"] = "writing to blobserver: " + err.Error()
			return
		}
	}
	ret["got"] = got
}
Example #25
0
func (sh *Handler) serveQuery(rw http.ResponseWriter, req *http.Request) {
	defer httputil.RecoverJSON(rw, req)

	var sq SearchQuery
	if err := sq.fromHTTP(req); err != nil {
		httputil.ServeJSONError(rw, err)
		return
	}

	sr, err := sh.Query(&sq)
	if err != nil {
		httputil.ServeJSONError(rw, err)
		return
	}

	httputil.ReturnJSON(rw, sr)
}
Example #26
0
func (sh *Handler) serveSignerAttrValue(rw http.ResponseWriter, req *http.Request) {
	ret := jsonMap()
	defer httputil.ReturnJSON(rw, ret)
	defer setPanicError(ret)

	signer := blobref.MustParse(mustGet(req, "signer"))
	attr := mustGet(req, "attr")
	value := mustGet(req, "value")
	pn, err := sh.index.PermanodeOfSignerAttrValue(signer, attr, value)
	if err != nil {
		ret["error"] = err.Error()
	} else {
		ret["permanode"] = pn.String()

		dr := sh.NewDescribeRequest()
		dr.Describe(pn, 2)
		dr.PopulateJSON(ret)
	}
}
Example #27
0
func (hh *HelpHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	suffix := httputil.PathSuffix(req)
	if !httputil.IsGet(req) {
		http.Error(rw, "Illegal help method.", http.StatusMethodNotAllowed)
		return
	}
	switch suffix {
	case "":
		if clientConfig := req.FormValue("clientConfig"); clientConfig != "" {
			if clientConfigOnly, err := strconv.ParseBool(clientConfig); err == nil && clientConfigOnly {
				httputil.ReturnJSON(rw, hh.clientConfig)
				return
			}
		}
		hh.serveHelpHTML(rw, req)
	default:
		http.Error(rw, "Illegal help path.", http.StatusNotFound)
	}
}
Example #28
0
func (sh *Handler) serveRecentPermanodes(rw http.ResponseWriter, req *http.Request) {
	ret := jsonMap()
	defer httputil.ReturnJSON(rw, ret)

	ch := make(chan *Result)
	errch := make(chan error)
	go func() {
		errch <- sh.index.GetRecentPermanodes(ch, sh.owner, 50)
	}()

	dr := sh.NewDescribeRequest()

	recent := jsonMapList()
	for res := range ch {
		dr.Describe(res.BlobRef, 2)
		jm := jsonMap()
		jm["blobref"] = res.BlobRef.String()
		jm["owner"] = res.Signer.String()
		t := time.Unix(res.LastModTime, 0).UTC()
		jm["modtime"] = t.Format(time.RFC3339)
		recent = append(recent, jm)
	}

	err := <-errch
	if err != nil {
		// TODO: return error status code
		ret["error"] = err.Error()
		return
	}

	ret["recent"] = recent

	thumbSize := 0
	if req.FormValue("thumbnails") != "" {
		thumbSize = 50
		if i, _ := strconv.Atoi(req.FormValue("thumbnails")); i >= 25 && i < 800 {
			thumbSize = i
		}
	}
	dr.populateJSONThumbnails(ret, thumbSize)
}
Example #29
0
// handleSearch runs the requested search query against the search handler, and
// if the results are within the domain allowed by the master query, forwards them
// back to the client.
func (a *Handler) handleSearch(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		camhttputil.BadRequestError(w, camhttputil.InvalidMethodError{}.Error())
		return
	}
	if a.sh == nil {
		http.Error(w, "app proxy has no search handler", 500)
		return
	}
	a.masterQueryMu.RLock()
	if a.masterQuery == nil {
		http.Error(w, "search is not allowed", http.StatusForbidden)
		a.masterQueryMu.RUnlock()
		return
	}
	a.masterQueryMu.RUnlock()
	var sq search.SearchQuery
	if err := sq.FromHTTP(r); err != nil {
		camhttputil.ServeJSONError(w, err)
		return
	}
	sr, err := a.sh.Query(&sq)
	if err != nil {
		camhttputil.ServeJSONError(w, err)
		return
	}
	// check this search is in the allowed domain
	if !a.allowProxySearchResponse(sr) {
		// there's a chance our domainBlobs cache is expired so let's
		// refresh it and retry, but no more than once per minute.
		if err := a.refreshDomainBlobs(); err != nil {
			http.Error(w, "search scope is forbidden", http.StatusForbidden)
			return
		}
		if !a.allowProxySearchResponse(sr) {
			http.Error(w, "search scope is forbidden", http.StatusForbidden)
			return
		}
	}
	camhttputil.ReturnJSON(w, sr)
}
Example #30
0
func (sh *Handler) serveDescribe(rw http.ResponseWriter, req *http.Request) {
	defer httputil.RecoverJSON(rw, req)
	var dr DescribeRequest
	dr.fromHTTP(req)

	sh.initDescribeRequest(&dr)

	if dr.BlobRef.Valid() {
		dr.Describe(dr.BlobRef, dr.depth())
	}
	for _, br := range dr.BlobRefs {
		dr.Describe(br, dr.depth())
	}

	metaMap, err := dr.metaMapThumbs(thumbnailSize(req))
	if err != nil {
		httputil.ServeJSONError(rw, err)
		return
	}
	httputil.ReturnJSON(rw, &DescribeResponse{metaMap})
}