mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-21 18:37:45 +08:00
fix(gw): validate requested CAR version (#8835)
* 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 <gus@gus.dev> Co-authored-by: Gus Eggert <gus@gus.dev>
This commit is contained in:
parent
46c3689b75
commit
5fa556945e
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@ -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()) {
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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:
|
||||
@ -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) {
|
||||
|
||||
@ -2,6 +2,7 @@ package corehttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user