Skip to content

Commit

Permalink
fix(gw): validate requested CAR version (#8835)
Browse files Browse the repository at this point in the history
* fix(gw): validate requested CAR version

This adds validation of 'application/vnd.ipld.car;version=n' passed
in the Accept header by HTTP clients to align Gateway behavior with
the spec submitted to IANA.

* test: fix comment in test/sharness/t0118-gateway-car.sh

Co-authored-by: Gus Eggert <[email protected]>

Co-authored-by: Gus Eggert <[email protected]>
  • Loading branch information
lidel and guseggert authored Apr 1, 2022
1 parent 46c3689 commit 5fa5569
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 11 deletions.
30 changes: 20 additions & 10 deletions core/corehttp/gateway_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"html/template"
"io"
"mime"
"net/http"
"net/url"
"os"
Expand Down Expand Up @@ -348,7 +349,11 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
}

// Detect when explicit Accept header or ?format parameter are present
responseFormat := customResponseFormat(r)
responseFormat, formatParams, err := customResponseFormat(r)
if err != nil {
webError(w, "error while processing the Accept header", err, http.StatusBadRequest)
return
}

// Finish early if client already has matching Etag
if r.Header.Get("If-None-Match") == getEtag(r, resolvedPath.Cid()) {
Expand Down Expand Up @@ -389,9 +394,10 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
logger.Debugw("serving raw block", "path", contentPath)
i.serveRawBlock(w, r, resolvedPath.Cid(), contentPath, begin)
return
case "application/vnd.ipld.car", "application/vnd.ipld.car; version=1":
case "application/vnd.ipld.car":
logger.Debugw("serving car stream", "path", contentPath)
i.serveCar(w, r, resolvedPath.Cid(), contentPath, begin)
carVersion := formatParams["version"]
i.serveCar(w, r, resolvedPath.Cid(), contentPath, carVersion, begin)
return
default: // catch-all for unsuported application/vnd.*
err := fmt.Errorf("unsupported format %q", responseFormat)
Expand Down Expand Up @@ -761,8 +767,8 @@ func getFilename(contentPath ipath.Path) string {
func getEtag(r *http.Request, cid cid.Cid) string {
prefix := `"`
suffix := `"`
responseFormat := customResponseFormat(r)
if responseFormat != "" {
responseFormat, _, err := customResponseFormat(r)
if err == nil && responseFormat != "" {
// application/vnd.ipld.foo → foo
f := responseFormat[strings.LastIndex(responseFormat, ".")+1:]
// Etag: "cid.foo" (gives us nice compression together with Content-Disposition in block (raw) and car responses)
Expand All @@ -773,14 +779,14 @@ func getEtag(r *http.Request, cid cid.Cid) string {
}

// return explicit response format if specified in request as query parameter or via Accept HTTP header
func customResponseFormat(r *http.Request) string {
func customResponseFormat(r *http.Request) (mediaType string, params map[string]string, err error) {
if formatParam := r.URL.Query().Get("format"); formatParam != "" {
// translate query param to a content type
switch formatParam {
case "raw":
return "application/vnd.ipld.raw"
return "application/vnd.ipld.raw", nil, nil
case "car":
return "application/vnd.ipld.car"
return "application/vnd.ipld.car", nil, nil
}
}
// Browsers and other user agents will send Accept header with generic types like:
Expand All @@ -789,10 +795,14 @@ func customResponseFormat(r *http.Request) string {
for _, accept := range r.Header.Values("Accept") {
// respond to the very first ipld content type
if strings.HasPrefix(accept, "application/vnd.ipld") {
return accept
mediatype, params, err := mime.ParseMediaType(accept)
if err != nil {
return "", nil, err
}
return mediatype, params, nil
}
}
return ""
return "", nil, nil
}

func (i *gatewayHandler) searchUpTreeFor404(r *http.Request, contentPath ipath.Path) (ipath.Resolved, string, error) {
Expand Down
12 changes: 11 additions & 1 deletion core/corehttp/gateway_handler_car.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package corehttp

import (
"context"
"fmt"
"net/http"
"time"

Expand All @@ -14,10 +15,19 @@ import (
)

// serveCar returns a CAR stream for specific DAG+selector
func (i *gatewayHandler) serveCar(w http.ResponseWriter, r *http.Request, rootCid cid.Cid, contentPath ipath.Path, begin time.Time) {
func (i *gatewayHandler) serveCar(w http.ResponseWriter, r *http.Request, rootCid cid.Cid, contentPath ipath.Path, carVersion string, begin time.Time) {
ctx, cancel := context.WithCancel(r.Context())
defer cancel()

switch carVersion {
case "": // noop, client does not care about version
case "1": // noop, we support this
default:
err := fmt.Errorf("only version=1 is supported")
webError(w, "unsupported CAR version", err, http.StatusBadRequest)
return
}

// Set Content-Disposition
name := rootCid.String() + ".car"
setContentDispositionHeader(w, name, "attachment")
Expand Down
15 changes: 15 additions & 0 deletions test/sharness/t0118-gateway-car.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,25 @@ test_launch_ipfs_daemon_without_network
# explicit version=1
test_expect_success "GET for application/vnd.ipld.raw version=1 returns a CARv1 stream" '
ipfs dag import test-dag.car &&
curl -sX GET -H "Accept: application/vnd.ipld.car;version=1" "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT_DIR_CID/subdir/ascii.txt" -o gateway-header-v1.car &&
test_cmp deterministic.car gateway-header-v1.car
'

# explicit version=1 with whitepace
test_expect_success "GET for application/vnd.ipld.raw version=1 returns a CARv1 stream (with whitespace)" '
ipfs dag import test-dag.car &&
curl -sX GET -H "Accept: application/vnd.ipld.car; version=1" "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT_DIR_CID/subdir/ascii.txt" -o gateway-header-v1.car &&
test_cmp deterministic.car gateway-header-v1.car
'

# explicit version=2
test_expect_success "GET for application/vnd.ipld.raw version=2 returns HTTP 400 Bad Request error" '
curl -svX GET -H "Accept: application/vnd.ipld.car;version=2" "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT_DIR_CID/subdir/ascii.txt" > curl_output 2>&1 &&
cat curl_output &&
grep "400 Bad Request" curl_output &&
grep "unsupported CAR version" curl_output
'

# GET unixfs directory as a CAR with DAG and some selector

# TODO: this is basic test for "full" selector, we will add support for custom ones in https://github.com/ipfs/go-ipfs/issues/8769
Expand Down

0 comments on commit 5fa5569

Please sign in to comment.