diff --git a/core/corehttp/gateway_handler.go b/core/corehttp/gateway_handler.go index eca2efff610..6d90dd0080a 100644 --- a/core/corehttp/gateway_handler.go +++ b/core/corehttp/gateway_handler.go @@ -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) { diff --git a/core/corehttp/gateway_handler_car.go b/core/corehttp/gateway_handler_car.go index 5f0f2117fc7..c6587e564f4 100644 --- a/core/corehttp/gateway_handler_car.go +++ b/core/corehttp/gateway_handler_car.go @@ -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") diff --git a/test/sharness/t0118-gateway-car.sh b/test/sharness/t0118-gateway-car.sh index 9cdb5aec522..796c3c33947 100755 --- a/test/sharness/t0118-gateway-car.sh +++ b/test/sharness/t0118-gateway-car.sh @@ -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