Skip to content

Commit

Permalink
wip: fix X-Ipfs-Root
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed Jun 8, 2023
1 parent a93ce13 commit 58b93ae
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 91 deletions.
78 changes: 51 additions & 27 deletions gateway/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,29 @@ func (i *handler) optionsHandler(w http.ResponseWriter, r *http.Request) {
i.addUserHeaders(w) // return all custom headers (including CORS ones, if set)
}

type requestData struct {
// Defined for all requests.
begin time.Time
logger *zap.SugaredLogger
contentPath ipath.Path
responseFormat string
responseParams map[string]string

// Defined for non IPNS Record requests.
immutablePath ImmutablePath

// Defined if resolution has already happened.
pathMetadata *ContentPathMetadata
resolvedPath *ImmutablePath
}

func (rq *requestData) maybeResolvedPath() ImmutablePath {
if rq.resolvedPath != nil {
return *rq.resolvedPath
}
return rq.immutablePath
}

func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) {
begin := time.Now()

Expand Down Expand Up @@ -223,25 +246,32 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) {
return
}

rq := &requestData{
begin: begin,
logger: logger,
contentPath: contentPath,
responseFormat: responseFormat,
responseParams: formatParams,
}

// IPNS Record response format can be handled now, since (1) it needs the
// non-resolved mutable path, and (2) has custom If-None-Match header handling
// due to custom ETag.
if responseFormat == ipnsRecordResponseFormat {
logger.Debugw("serving ipns record", "path", contentPath)
success = i.serveIpnsRecord(r.Context(), w, r, contentPath, begin, logger)
success = i.serveIpnsRecord(r.Context(), w, r, rq)
return
}

var immutableContentPath ImmutablePath
if contentPath.Mutable() {
immutableContentPath, err = i.backend.ResolveMutable(r.Context(), contentPath)
rq.immutablePath, err = i.backend.ResolveMutable(r.Context(), contentPath)
if err != nil {
err = fmt.Errorf("failed to resolve %s: %w", debugStr(contentPath.String()), err)
i.webError(w, r, err, http.StatusInternalServerError)
return
}
} else {
immutableContentPath, err = NewImmutablePath(contentPath)
rq.immutablePath, err = NewImmutablePath(contentPath)
if err != nil {
err = fmt.Errorf("path was expected to be immutable, but was not %s: %w", debugStr(contentPath.String()), err)
i.webError(w, r, err, http.StatusInternalServerError)
Expand All @@ -254,36 +284,28 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) {
// header handling due to custom ETag.
if responseFormat == carResponseFormat {
logger.Debugw("serving car stream", "path", contentPath)
carVersion := formatParams["version"]
success = i.serveCAR(r.Context(), w, r, immutableContentPath, contentPath, carVersion, begin)
success = i.serveCAR(r.Context(), w, r, rq)
return
}

// Detect when If-None-Match HTTP header allows returning HTTP 304 Not Modified.
ifNoneMatchResolvedPath, handled := i.handleIfNoneMatch(w, r, responseFormat, contentPath, immutableContentPath)
if handled {
if i.handleIfNoneMatch(w, r, rq) {
return
}

// If we already did the path resolution no need to do it again
maybeResolvedImPath := immutableContentPath
if ifNoneMatchResolvedPath != nil {
maybeResolvedImPath = *ifNoneMatchResolvedPath
}

// Support custom response formats passed via ?format or Accept HTTP header
switch responseFormat {
case "", jsonResponseFormat, cborResponseFormat:
success = i.serveDefaults(r.Context(), w, r, maybeResolvedImPath, immutableContentPath, contentPath, begin, responseFormat, logger)
success = i.serveDefaults(r.Context(), w, r, rq)
case rawResponseFormat:
logger.Debugw("serving raw block", "path", contentPath)
success = i.serveRawBlock(r.Context(), w, r, maybeResolvedImPath, contentPath, begin)
success = i.serveRawBlock(r.Context(), w, r, rq)
case tarResponseFormat:
logger.Debugw("serving tar file", "path", contentPath)
success = i.serveTAR(r.Context(), w, r, maybeResolvedImPath, contentPath, begin, logger)
success = i.serveTAR(r.Context(), w, r, rq)
case dagJsonResponseFormat, dagCborResponseFormat:
logger.Debugw("serving codec", "path", contentPath)
success = i.serveCodec(r.Context(), w, r, maybeResolvedImPath, contentPath, begin, responseFormat)
success = i.serveCodec(r.Context(), w, r, rq)
default: // catch-all for unsuported application/vnd.*
err := fmt.Errorf("unsupported format %q", responseFormat)
i.webError(w, r, err, http.StatusBadRequest)
Expand Down Expand Up @@ -646,40 +668,42 @@ func debugStr(path string) string {
return q
}

func (i *handler) handleIfNoneMatch(w http.ResponseWriter, r *http.Request, responseFormat string, contentPath ipath.Path, imPath ImmutablePath) (*ImmutablePath, bool) {
func (i *handler) handleIfNoneMatch(w http.ResponseWriter, r *http.Request, rq *requestData) bool {
// Detect when If-None-Match HTTP header allows returning HTTP 304 Not Modified
if ifNoneMatch := r.Header.Get("If-None-Match"); ifNoneMatch != "" {
pathMetadata, err := i.backend.ResolvePath(r.Context(), imPath)
pathMetadata, err := i.backend.ResolvePath(r.Context(), rq.immutablePath)
if err != nil {
err = fmt.Errorf("failed to resolve %s: %w", debugStr(contentPath.String()), err)
err = fmt.Errorf("failed to resolve %s: %w", debugStr(rq.contentPath.String()), err)
i.webError(w, r, err, http.StatusInternalServerError)
return nil, true
return true
}

resolvedPath := pathMetadata.LastSegment
pathCid := resolvedPath.Cid()

// Checks against both file, dir listing, and dag index Etags.
// This is an inexpensive check, and it happens before we do any I/O.
cidEtag := getEtag(r, pathCid, responseFormat)
cidEtag := getEtag(r, pathCid, rq.responseFormat)
dirEtag := getDirListingEtag(pathCid)
dagEtag := getDagIndexEtag(pathCid)

if etagMatch(ifNoneMatch, cidEtag, dirEtag, dagEtag) {
// Finish early if client already has a matching Etag
w.WriteHeader(http.StatusNotModified)
return nil, true
return true
}

resolvedImPath, err := NewImmutablePath(resolvedPath)
if err != nil {
i.webError(w, r, err, http.StatusInternalServerError)
return nil, true
return true
}

return &resolvedImPath, true
rq.pathMetadata = &pathMetadata
rq.resolvedPath = &resolvedImPath
return true
}
return nil, false
return false
}

// check if request was for one of known explicit formats,
Expand Down
18 changes: 10 additions & 8 deletions gateway/handler_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,25 @@ import (
"net/http"
"time"

ipath "github.com/ipfs/boxo/coreiface/path"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)

// serveRawBlock returns bytes behind a raw block
func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *http.Request, imPath ImmutablePath, contentPath ipath.Path, begin time.Time) bool {
ctx, span := spanTrace(ctx, "Handler.ServeRawBlock", trace.WithAttributes(attribute.String("path", imPath.String())))
func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *http.Request, rq *requestData) bool {
ctx, span := spanTrace(ctx, "Handler.ServeRawBlock", trace.WithAttributes(attribute.String("path", rq.immutablePath.String())))
defer span.End()

pathMetadata, data, err := i.backend.GetBlock(ctx, imPath)
if !i.handleRequestErrors(w, r, contentPath, err) {
pathMetadata, data, err := i.backend.GetBlock(ctx, rq.maybeResolvedPath())
if !i.handleRequestErrors(w, r, rq.contentPath, err) {
return false
}
if rq.pathMetadata == nil {
rq.pathMetadata = &pathMetadata
}
defer data.Close()

setIpfsRootsHeader(w, pathMetadata)
setIpfsRootsHeader(w, *rq.pathMetadata)

blockCid := pathMetadata.LastSegment.Cid()

Expand All @@ -35,7 +37,7 @@ func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *h
setContentDispositionHeader(w, name, "attachment")

// Set remaining headers
modtime := addCacheControlHeaders(w, r, contentPath, blockCid, rawResponseFormat)
modtime := addCacheControlHeaders(w, r, rq.contentPath, blockCid, rawResponseFormat)
w.Header().Set("Content-Type", rawResponseFormat)
w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^)

Expand All @@ -45,7 +47,7 @@ func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *h

if dataSent {
// Update metrics
i.rawBlockGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
i.rawBlockGetMetric.WithLabelValues(rq.contentPath.Namespace()).Observe(time.Since(rq.begin).Seconds())
}

return dataSent
Expand Down
21 changes: 10 additions & 11 deletions gateway/handler_car.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"time"

"github.com/cespare/xxhash/v2"
ipath "github.com/ipfs/boxo/coreiface/path"
"github.com/ipfs/go-cid"

"go.opentelemetry.io/otel/attribute"
Expand All @@ -24,14 +23,14 @@ const (
)

// serveCAR returns a CAR stream for specific DAG+selector
func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.Request, imPath ImmutablePath, contentPath ipath.Path, carVersion string, begin time.Time) bool {
ctx, span := spanTrace(ctx, "Handler.ServeCAR", trace.WithAttributes(attribute.String("path", imPath.String())))
func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.Request, rq *requestData) bool {
ctx, span := spanTrace(ctx, "Handler.ServeCAR", trace.WithAttributes(attribute.String("path", rq.immutablePath.String())))
defer span.End()

ctx, cancel := context.WithCancel(ctx)
defer cancel()

switch carVersion {
switch rq.responseParams["version"] {
case "": // noop, client does not care about version
case "1": // noop, we support this
default:
Expand All @@ -46,7 +45,7 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R
return false
}

rootCid, lastSegment, err := getCarRootCidAndLastSegment(imPath)
rootCid, lastSegment, err := getCarRootCidAndLastSegment(rq.immutablePath)
if err != nil {
i.webError(w, r, err, http.StatusInternalServerError)
return false
Expand All @@ -70,10 +69,10 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R
setContentDispositionHeader(w, name, "attachment")

// Set Cache-Control (same logic as for a regular files)
addCacheControlHeaders(w, r, contentPath, rootCid, carResponseFormat)
addCacheControlHeaders(w, r, rq.contentPath, rootCid, carResponseFormat)

// Generate the CAR Etag.
etag := getCarEtag(imPath, params, rootCid)
etag := getCarEtag(rq.immutablePath, params, rootCid)
w.Header().Set("Etag", etag)

// Terminate early if Etag matches. We cannot rely on handleIfNoneMatch since
Expand All @@ -83,8 +82,8 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R
return false
}

carFile, err := i.backend.GetCAR(ctx, imPath, params)
if !i.handleRequestErrors(w, r, contentPath, err) {
carFile, err := i.backend.GetCAR(ctx, rq.immutablePath, params)
if !i.handleRequestErrors(w, r, rq.contentPath, err) {
return false
}
defer carFile.Close()
Expand All @@ -102,7 +101,7 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R
streamErr := multierr.Combine(carErr, copyErr)
if streamErr != nil {
// Update fail metric
i.carStreamFailMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
i.carStreamFailMetric.WithLabelValues(rq.contentPath.Namespace()).Observe(time.Since(rq.begin).Seconds())

// We return error as a trailer, however it is not something browsers can access
// (https://github.com/mdn/browser-compat-data/issues/14703)
Expand All @@ -113,7 +112,7 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R
}

// Update metrics
i.carStreamGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
i.carStreamGetMetric.WithLabelValues(rq.contentPath.Namespace()).Observe(time.Since(rq.begin).Seconds())
return true
}

Expand Down
15 changes: 9 additions & 6 deletions gateway/handler_codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,23 @@ var contentTypeToExtension = map[string]string{
dagCborResponseFormat: ".cbor",
}

func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, imPath ImmutablePath, contentPath ipath.Path, begin time.Time, requestedContentType string) bool {
ctx, span := spanTrace(ctx, "Handler.ServeCodec", trace.WithAttributes(attribute.String("path", imPath.String()), attribute.String("requestedContentType", requestedContentType)))
func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, rq *requestData) bool {
ctx, span := spanTrace(ctx, "Handler.ServeCodec", trace.WithAttributes(attribute.String("path", rq.immutablePath.String()), attribute.String("requestedContentType", rq.responseFormat)))
defer span.End()

pathMetadata, data, err := i.backend.GetBlock(ctx, imPath)
if !i.handleRequestErrors(w, r, contentPath, err) {
pathMetadata, data, err := i.backend.GetBlock(ctx, rq.maybeResolvedPath())
if !i.handleRequestErrors(w, r, rq.contentPath, err) {
return false
}
if rq.pathMetadata == nil {
rq.pathMetadata = &pathMetadata
}
defer data.Close()

setIpfsRootsHeader(w, pathMetadata)
setIpfsRootsHeader(w, *rq.pathMetadata)

resolvedPath := pathMetadata.LastSegment
return i.renderCodec(ctx, w, r, resolvedPath, data, contentPath, begin, requestedContentType)
return i.renderCodec(ctx, w, r, resolvedPath, data, rq.contentPath, rq.begin, rq.responseFormat)
}

func (i *handler) renderCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, blockData io.ReadSeekCloser, contentPath ipath.Path, begin time.Time, requestedContentType string) bool {
Expand Down
Loading

0 comments on commit 58b93ae

Please sign in to comment.