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>
Marcin Rataj committed
Apr 1, 2022 at 18:12 UTC
5fa556945e2a9733f39e1bfcc242cba4c31c070b
3 files changed
+46
-11
core/corehttp/gateway_handler.go
+20
-10
@@ -5,6 +5,7 @@ import (
5
"fmt"
6
"html/template"
7
"io"
8
+ "mime"
9
"net/http"
10
"net/url"
11
"os"
@@ -348,7 +349,11 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
349
}
350
351
// Detect when explicit Accept header or ?format parameter are present
351
- responseFormat := customResponseFormat(r)
352
+ responseFormat, formatParams, err := customResponseFormat(r)
353
+ if err != nil {
354
+ webError(w, "error while processing the Accept header", err, http.StatusBadRequest)
355
+ return
356
+ }
357
358
// Finish early if client already has matching Etag
359
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
394
logger.Debugw("serving raw block", "path", contentPath)
395
i.serveRawBlock(w, r, resolvedPath.Cid(), contentPath, begin)
396
return
392
- case "application/vnd.ipld.car", "application/vnd.ipld.car; version=1":
397
+ case "application/vnd.ipld.car":
398
logger.Debugw("serving car stream", "path", contentPath)
394
- i.serveCar(w, r, resolvedPath.Cid(), contentPath, begin)
399
+ carVersion := formatParams["version"]
400
+ i.serveCar(w, r, resolvedPath.Cid(), contentPath, carVersion, begin)
401
return
402
default: // catch-all for unsuported application/vnd.*
403
err := fmt.Errorf("unsupported format %q", responseFormat)
@@ -761,8 +767,8 @@ func getFilename(contentPath ipath.Path) string {
767
func getEtag(r *http.Request, cid cid.Cid) string {
768
prefix := `"`
769
suffix := `"`
764
- responseFormat := customResponseFormat(r)
765
- if responseFormat != "" {
770
+ responseFormat, _, err := customResponseFormat(r)
771
+ if err == nil && responseFormat != "" {
772
// application/vnd.ipld.foo → foo
773
f := responseFormat[strings.LastIndex(responseFormat, ".")+1:]
774
// 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 {
779
}
780
781
// return explicit response format if specified in request as query parameter or via Accept HTTP header
776
-func customResponseFormat(r *http.Request) string {
782
+func customResponseFormat(r *http.Request) (mediaType string, params map[string]string, err error) {
783
if formatParam := r.URL.Query().Get("format"); formatParam != "" {
784
// translate query param to a content type
785
switch formatParam {
786
case "raw":
781
- return "application/vnd.ipld.raw"
787
+ return "application/vnd.ipld.raw", nil, nil
788
case "car":
783
- return "application/vnd.ipld.car"
789
+ return "application/vnd.ipld.car", nil, nil
790
}
791
}
792
// Browsers and other user agents will send Accept header with generic types like:
@@ -789,10 +795,14 @@ func customResponseFormat(r *http.Request) string {
795
for _, accept := range r.Header.Values("Accept") {
796
// respond to the very first ipld content type
797
if strings.HasPrefix(accept, "application/vnd.ipld") {
792
- return accept
798
+ mediatype, params, err := mime.ParseMediaType(accept)
799
+ if err != nil {
800
+ return "", nil, err
801
+ }
802
+ return mediatype, params, nil
803
}
804
}
795
- return ""
805
+ return "", nil, nil
806
}
807
808
func (i *gatewayHandler) searchUpTreeFor404(r *http.Request, contentPath ipath.Path) (ipath.Resolved, string, error) {
core/corehttp/gateway_handler_car.go
+11
-1
@@ -2,6 +2,7 @@ package corehttp
2
3
import (
4
"context"
5
+ "fmt"
6
"net/http"
7
"time"
8
@@ -14,10 +15,19 @@ import (
15
)
16
17
// serveCar returns a CAR stream for specific DAG+selector
17
-func (i *gatewayHandler) serveCar(w http.ResponseWriter, r *http.Request, rootCid cid.Cid, contentPath ipath.Path, begin time.Time) {
18
+func (i *gatewayHandler) serveCar(w http.ResponseWriter, r *http.Request, rootCid cid.Cid, contentPath ipath.Path, carVersion string, begin time.Time) {
19
ctx, cancel := context.WithCancel(r.Context())
20
defer cancel()
21
22
+ switch carVersion {
23
+ case "": // noop, client does not care about version
24
+ case "1": // noop, we support this
25
+ default:
26
+ err := fmt.Errorf("only version=1 is supported")
27
+ webError(w, "unsupported CAR version", err, http.StatusBadRequest)
28
+ return
29
+ }
30
+
31
// Set Content-Disposition
32
name := rootCid.String() + ".car"
33
setContentDispositionHeader(w, name, "attachment")
test/sharness/t0118-gateway-car.sh
+15
@@ -51,10 +51,25 @@ test_launch_ipfs_daemon_without_network
51
# explicit version=1
52
test_expect_success "GET for application/vnd.ipld.raw version=1 returns a CARv1 stream" '
53
ipfs dag import test-dag.car &&
54
+ 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 &&
55
+ test_cmp deterministic.car gateway-header-v1.car
56
+ '
57
+
58
+ # explicit version=1 with whitepace
59
+ test_expect_success "GET for application/vnd.ipld.raw version=1 returns a CARv1 stream (with whitespace)" '
60
+ ipfs dag import test-dag.car &&
61
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 &&
62
test_cmp deterministic.car gateway-header-v1.car
63
'
64
65
+ # explicit version=2
66
+ test_expect_success "GET for application/vnd.ipld.raw version=2 returns HTTP 400 Bad Request error" '
67
+ 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 &&
68
+ cat curl_output &&
69
+ grep "400 Bad Request" curl_output &&
70
+ grep "unsupported CAR version" curl_output
71
+ '
72
+
73
# GET unixfs directory as a CAR with DAG and some selector
74
75
# TODO: this is basic test for "full" selector, we will add support for custom ones in https://github.com/ipfs/go-ipfs/issues/8769