Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(gw): validate requested CAR version #8835

Merged
merged 2 commits into from
Apr 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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