diff --git a/Makefile b/Makefile index 303dea813..af272befd 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ test-cargateway: provision-cargateway fixtures.car gateway-conformance test-kubo-subdomains: provision-kubo gateway-conformance ./kubo-config.example.sh - ./gateway-conformance test --json reports/output.json --gateway-url http://127.0.0.1:8080 --subdomain-url http://localhost:8080 + ./gateway-conformance test --json reports/output.json --gateway-url http://127.0.0.1:8080 --subdomain-url http://example.com:8080 test-kubo: provision-kubo gateway-conformance ./gateway-conformance test --json reports/output.json --gateway-url http://127.0.0.1:8080 --specs -subdomain-gateway @@ -31,13 +31,16 @@ provision-kubo: find ./fixtures -name '*.car' -exec ipfs dag import --stats --pin-roots=false {} \; find ./fixtures -name '*.ipns-record' -exec sh -c 'ipfs routing put --allow-offline /ipns/$$(basename -s .ipns-record "{}" | cut -d'_' -f1) "{}"' \; -start-kubo-docker: stop-kubo-docker gateway-conformance - ./gateway-conformance extract-fixtures --dnslink=true --car=false --ipns=false --dir=.temp - docker pull ipfs/kubo:$(KUBO_VERSION) - docker run -d --rm --net=host --name $(KUBO_DOCKER_NAME) -e IPFS_NS_MAP="$(shell cat ./.temp/dnslinks.IPFS_NS_MAP)" -v ./fixtures:/fixtures ipfs/kubo:$(KUBO_VERSION) daemon --init --offline - @until docker exec $(KUBO_DOCKER_NAME) ipfs --api=/ip4/127.0.0.1/tcp/5001 dag stat /ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn >/dev/null 2>&1; do sleep 0.1; done - find ./fixtures -name '*.car' -exec docker exec $(KUBO_DOCKER_NAME) ipfs dag import --stats --pin-roots=false {} \; - find ./fixtures -name '*.ipns-record' -exec docker exec $(KUBO_DOCKER_NAME) sh -c 'ipfs routing put --allow-offline /ipns/$$(basename -s .ipns-record "{}" | cut -d'_' -f1) "{}"' \; +#start-kubo-docker: stop-kubo-docker gateway-conformance +# ./gateway-conformance extract-fixtures --dir=.temp/fixtures +# docker pull ipfs/kubo:$(KUBO_VERSION) +# docker run -d --rm --net=host --name $(KUBO_DOCKER_NAME) -v "$(shell realpath .temp/fixtures)":/fixtures -v kubo-config.example.sh:/container-init.d/001-config.sh ipfs/kubo:$(KUBO_VERSION) daemon --init --offline +# @until docker exec $(KUBO_DOCKER_NAME) ipfs --api=/ip4/127.0.0.1/tcp/5001 dag stat /ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn >/dev/null 2>&1; do sleep 0.1; done +# docker exec $(KUBO_DOCKER_NAME) find /fixtures -name '*.car' -exec ipfs dag import --stats --pin-roots=false {} \; +# docker exec $(KUBO_DOCKER_NAME) find /fixtures -name '*.ipns-record' -exec sh -c 'ipfs routing put --allow-offline /ipns/$$(basename -s .ipns-record "{}" | cut -d'_' -f1) "{}"' \; +# TODO: provision Kubo config at Gateway.PublicGateways to have subdomain gateway on example.com and also enable inlining on localhost +# See: https://github.com/ipfs/kubo/blob/a07852a3f0294974b802923fb136885ad077384e/.github/workflows/gateway-conformance.yml#L22-L34 +# (this is not as trivial as it sounds because Kubo does not apply config inrealtime, and a restart is required.) stop-kubo-docker: clean docker stop $(KUBO_DOCKER_NAME) || true diff --git a/cmd/gateway-conformance/main.go b/cmd/gateway-conformance/main.go index 1e234dda4..cc92ac090 100644 --- a/cmd/gateway-conformance/main.go +++ b/cmd/gateway-conformance/main.go @@ -91,7 +91,7 @@ func main() { Name: "subdomain-url", EnvVars: []string{"SUBDOMAIN_GATEWAY_URL"}, Usage: "URL of the HTTP Host that should be used when testing https://specs.ipfs.tech/http-gateways/subdomain-gateway/ functionality", - Value: "http://example.com:8080", // TODO: ideally, make these empty by default, and opt-in + Value: "http://example.com:8080", }, &cli.StringFlag{ Name: "json-output", @@ -108,7 +108,7 @@ func main() { &cli.StringFlag{ Name: "specs", EnvVars: []string{"SPECS"}, - Usage: "Optional explicit scope of tests to run. Accepts a 'spec' (test only this spec), a '+spec' (test also this immature spec), or a '-spec' (do not test this mature spec). Available spec presets: " + strings.Join(getAvailableSpecPresets(), ","), + Usage: "Adjust the scope of tests to run. Accepts a 'spec' (test only this spec), a '+spec' (test also this immature spec), or a '-spec' (do not test this mature spec). Available spec presets: " + strings.Join(getAvailableSpecPresets(), ","), Value: "", }, &cli.BoolFlag{ @@ -120,13 +120,24 @@ func main() { Action: func(cctx *cli.Context) error { env := os.Environ() verbose := cctx.Bool("verbose") + + // Set gateway URLs gatewayURL := cctx.String("gateway-url") subdomainGatewayURL := cctx.String("subdomain-url") - env = append(env, fmt.Sprintf("GATEWAY_URL=%s", gatewayURL)) + envGwURL := fmt.Sprintf("GATEWAY_URL=%s", gatewayURL) + if verbose { + fmt.Println(envGwURL) + } + env = append(env, envGwURL) if subdomainGatewayURL != "" { - env = append(env, fmt.Sprintf("SUBDOMAIN_GATEWAY_URL=%s", subdomainGatewayURL)) + envSubdomainGwURL := fmt.Sprintf("SUBDOMAIN_GATEWAY_URL=%s", subdomainGatewayURL) + if verbose { + fmt.Println(envSubdomainGwURL) + } + env = append(env, envSubdomainGwURL) } + // Set other parameters args := []string{"test", "./tests", "-test.v=test2json"} specs := cctx.String("specs") @@ -140,8 +151,8 @@ func main() { args = append(args, cctx.Args().Slice()...) fmt.Println("go " + strings.Join(args, " ")) - fmt.Println("ENV " + strings.Join(env, " ")) + // Execute tests against URLs output := &bytes.Buffer{} cmd := exec.Command("go", args...) cmd.Dir = tooling.Home() diff --git a/kubo-config.example.sh b/kubo-config.example.sh index b5aa74aa2..87e30da6d 100755 --- a/kubo-config.example.sh +++ b/kubo-config.example.sh @@ -1,4 +1,4 @@ -#! /usr/bin/env bash +#!/usr/bin/env bash FIXTURES_PATH=${1:-$(pwd)} @@ -15,7 +15,7 @@ ipfs config --json Gateway.PublicGateways '{ } }' -export IPFS_NS_MAP="$(cat "${FIXTURES_PATH}/dnslinks.json" | jq -r '.domains | to_entries | map("\(.key):\(.value)") | join(",")')" +export IPFS_NS_MAP="$(cat "${FIXTURES_PATH}/dnslinks.IPFS_NS_MAP")" echo "Set the following IPFS_NS_MAP before starting the kubo daemon:" echo "IPFS_NS_MAP=${IPFS_NS_MAP}" diff --git a/tests/dnslink_gateway_test.go b/tests/dnslink_gateway_test.go index bfa5ba6a4..adbc9c023 100644 --- a/tests/dnslink_gateway_test.go +++ b/tests/dnslink_gateway_test.go @@ -7,7 +7,6 @@ import ( "github.com/ipfs/gateway-conformance/tooling/car" . "github.com/ipfs/gateway-conformance/tooling/check" "github.com/ipfs/gateway-conformance/tooling/dnslink" - "github.com/ipfs/gateway-conformance/tooling/helpers" "github.com/ipfs/gateway-conformance/tooling/specs" . "github.com/ipfs/gateway-conformance/tooling/test" ) @@ -74,5 +73,5 @@ func TestDNSLinkGatewayUnixFSDirectoryListing(t *testing.T) { }, } - RunWithSpecs(t, helpers.UnwrapSubdomainTests(t, tests), specs.DNSLinkGateway) + RunWithSpecs(t, tests, specs.DNSLinkGateway) } diff --git a/tests/metadata_test.go b/tests/metadata_test.go index 738d44aef..b76c55e6e 100644 --- a/tests/metadata_test.go +++ b/tests/metadata_test.go @@ -12,8 +12,8 @@ func logGatewayURL(t *testing.T) { GatewayURL string `json:"gateway_url"` SubdomainGatewayURL string `json:"subdomain_gateway_url"` }{ - GatewayURL: test.GatewayURL, - SubdomainGatewayURL: test.SubdomainGatewayURL, + GatewayURL: test.GatewayURL().String(), + SubdomainGatewayURL: test.SubdomainGatewayURL().String(), }) } diff --git a/tests/redirects_file_test.go b/tests/redirects_file_test.go index 1a667b3d9..25c9ee3d1 100644 --- a/tests/redirects_file_test.go +++ b/tests/redirects_file_test.go @@ -1,14 +1,12 @@ package tests import ( - "net/url" "testing" "github.com/ipfs/gateway-conformance/tooling" "github.com/ipfs/gateway-conformance/tooling/car" . "github.com/ipfs/gateway-conformance/tooling/check" "github.com/ipfs/gateway-conformance/tooling/dnslink" - "github.com/ipfs/gateway-conformance/tooling/helpers" "github.com/ipfs/gateway-conformance/tooling/specs" . "github.com/ipfs/gateway-conformance/tooling/test" . "github.com/ipfs/gateway-conformance/tooling/tmpl" @@ -28,20 +26,16 @@ func TestRedirectsFileSupport(t *testing.T) { // Redirects require origin isolation (https://specs.ipfs.tech/http-gateways/web-redirects-file/) // This means we only run these tests against origins explicitly passed via --subdomain-url - u, err := url.Parse(SubdomainGatewayURL) - if err != nil { - t.Fatal(err) - } - - redirectDirBaseURL := Fmt("{{scheme}}://{{cid}}.ipfs.{{host}}", u.Scheme, redirectDirCID, u.Host) + u := SubdomainGatewayURL() - // TODO hostHdr := Fmt("{{cid}}.ipfs.{{host}}", redirectDirCID, u.Host) + dirCIDInSubdomain := Fmt("{{cid}}.ipfs.{{host}}", redirectDirCID, u.Host) tests = append(tests, SugarTests{ { - Name: "request for $REDIRECTS_DIR_HOSTNAME/redirect-one redirects with default of 301, per _redirects file", + Name: "request for {cid}.ipfs.example.com/redirect-one redirects with default of 301, per _redirects file", Request: Request(). - URL("{{url}}/redirect-one", redirectDirBaseURL), + Header("Host", dirCIDInSubdomain). + Path("/redirect-one"), Response: Expect(). Status(301). Headers( @@ -49,9 +43,10 @@ func TestRedirectsFileSupport(t *testing.T) { ), }, { - Name: "request for $REDIRECTS_DIR_HOSTNAME/301-redirect-one redirects with 301, per _redirects file", + Name: "request for {cid}.ipfs.example.com/301-redirect-one redirects with 301, per _redirects file", Request: Request(). - URL("{{url}}/301-redirect-one", redirectDirBaseURL), + Header("Host", dirCIDInSubdomain). + Path("/301-redirect-one"), Response: Expect(). Status(301). Headers( @@ -59,9 +54,10 @@ func TestRedirectsFileSupport(t *testing.T) { ), }, { - Name: "request for $REDIRECTS_DIR_HOSTNAME/302-redirect-two redirects with 302, per _redirects file", + Name: "request for {cid}.ipfs.example.com/302-redirect-two redirects with 302, per _redirects file", Request: Request(). - URL("{{url}}/302-redirect-two", redirectDirBaseURL), + Header("Host", dirCIDInSubdomain). + Path("/302-redirect-two"), Response: Expect(). Status(302). Headers( @@ -69,17 +65,19 @@ func TestRedirectsFileSupport(t *testing.T) { ), }, { - Name: "request for $REDIRECTS_DIR_HOSTNAME/200-index returns 200, per _redirects file", + Name: "request for {cid}.ipfs.example.com/200-index returns 200, per _redirects file", Request: Request(). - URL("{{url}}/200-index", redirectDirBaseURL), + Header("Host", dirCIDInSubdomain). + Path("/200-index"), Response: Expect(). Status(200). Body(Contains("my index")), }, { - Name: "request for $REDIRECTS_DIR_HOSTNAME/posts/:year/:month/:day/:title redirects with 301 and placeholders, per _redirects file", + Name: "request for {cid}.ipfs.example.com/posts/:year/:month/:day/:title redirects with 301 and placeholders, per _redirects file", Request: Request(). - URL("{{url}}/posts/2022/01/01/hello-world", redirectDirBaseURL), + Header("Host", dirCIDInSubdomain). + Path("/posts/2022/01/01/hello-world"), Response: Expect(). Status(301). Headers( @@ -87,9 +85,10 @@ func TestRedirectsFileSupport(t *testing.T) { ), }, { - Name: "request for $REDIRECTS_DIR_HOSTNAME/splat/one.html redirects with 301 and splat placeholder, per _redirects file", + Name: "request for {cid}.ipfs.example.com/splat/one.html redirects with 301 and splat placeholder, per _redirects file", Request: Request(). - URL("{{url}}/splat/one.html", redirectDirBaseURL), + Header("Host", dirCIDInSubdomain). + Path("/splat/one.html"), Response: Expect(). Status(301). Headers( @@ -97,9 +96,10 @@ func TestRedirectsFileSupport(t *testing.T) { ), }, { - Name: "request for $REDIRECTS_DIR_HOSTNAME/not-found/has-no-redirects-entry returns custom 404, per _redirects file", + Name: "request for {cid}.ipfs.example.com/not-found/has-no-redirects-entry returns custom 404, per _redirects file", Request: Request(). - URL("{{url}}/not-found/has-no-redirects-entry", redirectDirBaseURL), + Header("Host", dirCIDInSubdomain). + Path("/not-found/has-no-redirects-entry"), Response: Expect(). Status(404). Headers( @@ -109,9 +109,10 @@ func TestRedirectsFileSupport(t *testing.T) { Body(Contains(custom404.ReadFile())), }, { - Name: "request for $REDIRECTS_DIR_HOSTNAME/gone/has-no-redirects-entry returns custom 410, per _redirects file", + Name: "request for {cid}.ipfs.example.com/gone/has-no-redirects-entry returns custom 410, per _redirects file", Request: Request(). - URL("{{url}}/gone/has-no-redirects-entry", redirectDirBaseURL), + Header("Host", dirCIDInSubdomain). + Path("/gone/has-no-redirects-entry"), Response: Expect(). Status(410). Headers( @@ -121,9 +122,10 @@ func TestRedirectsFileSupport(t *testing.T) { Body(Contains(custom410.ReadFile())), }, { - Name: "request for $REDIRECTS_DIR_HOSTNAME/unavail/has-no-redirects-entry returns custom 451, per _redirects file", + Name: "request for {cid}.ipfs.example.com/unavail/has-no-redirects-entry returns custom 451, per _redirects file", Request: Request(). - URL("{{url}}/unavail/has-no-redirects-entry", redirectDirBaseURL), + Header("Host", dirCIDInSubdomain). + Path("/unavail/has-no-redirects-entry"), Response: Expect(). Status(451). Headers( @@ -133,9 +135,10 @@ func TestRedirectsFileSupport(t *testing.T) { Body(Contains(custom451.ReadFile())), }, { - Name: "request for $REDIRECTS_DIR_HOSTNAME/catch-all returns 200, per _redirects file", + Name: "request for {cid}.ipfs.example.com/catch-all returns 200, per _redirects file", Request: Request(). - URL("{{url}}/catch-all", redirectDirBaseURL), + Header("Host", dirCIDInSubdomain). + Path("/catch-all"), Response: Expect(). Status(200). Body(Contains("my index")), @@ -144,17 +147,18 @@ func TestRedirectsFileSupport(t *testing.T) { // # Invalid file, containing forced redirect invalidRedirectsDirCID := fixture.MustGetNode("forced").Base32Cid() - invalidDirBaseURL := Fmt("{{scheme}}://{{cid}}.ipfs.{{host}}", u.Scheme, invalidRedirectsDirCID, u.Host) + invalidDirSubdomain := Fmt("{{cid}}.ipfs.{{host}}", invalidRedirectsDirCID, u.Host) tooLargeRedirectsDirCID := fixture.MustGetNode("too-large").Base32Cid() - tooLargeDirBaseURL := Fmt("{{scheme}}://{{cid}}.ipfs.{{host}}", u.Scheme, tooLargeRedirectsDirCID, u.Host) + tooLargeDirSubdomain := Fmt("{{cid}}.ipfs.{{host}}", tooLargeRedirectsDirCID, u.Host) tests = append(tests, SugarTests{ { Name: "invalid file: request for $INVALID_REDIRECTS_DIR_HOSTNAME/not-found returns error about invalid redirects file", Hint: `if accessing a path that doesn't exist, read _redirects and fail parsing, and return error`, Request: Request(). - URL("{{url}}/not-found", invalidDirBaseURL), + Header("Host", invalidDirSubdomain). + Path("/not-found"), Response: Expect(). Status(500). Body( @@ -169,7 +173,8 @@ func TestRedirectsFileSupport(t *testing.T) { Name: "invalid file: request for $TOO_LARGE_REDIRECTS_DIR_HOSTNAME/not-found returns error about too large redirects file", Hint: `if accessing a path that doesn't exist and _redirects file is too large, return error`, Request: Request(). - URL("{{url}}/not-found", tooLargeDirBaseURL), + Header("Host", tooLargeDirSubdomain). + Path("/not-found"), Response: Expect(). Status(500). Body( @@ -184,21 +189,22 @@ func TestRedirectsFileSupport(t *testing.T) { // # With CRLF line terminator newlineRedirectsDirCID := fixture.MustGetNode("newlines").Base32Cid() - newlineBaseURL := Fmt("{{scheme}}://{{cid}}.ipfs.{{host}}", u.Scheme, newlineRedirectsDirCID, u.Host) + newlineHost := Fmt("{{cid}}.ipfs.{{host}}", newlineRedirectsDirCID, u.Host) // # Good codes goodRedirectDirCID := fixture.MustGetNode("good-codes").Base32Cid() - goodRedirectDirBaseURL := Fmt("{{scheme}}://{{cid}}.ipfs.{{host}}", u.Scheme, goodRedirectDirCID, u.Host) + goodRedirectDirHost := Fmt("{{cid}}.ipfs.{{host}}", goodRedirectDirCID, u.Host) // # Bad codes badRedirectDirCID := fixture.MustGetNode("bad-codes").Base32Cid() - badRedirectDirBaseURL := Fmt("{{scheme}}://{{cid}}.ipfs.{{host}}", u.Scheme, badRedirectDirCID, u.Host) + badRedirectDirHost := Fmt("{{cid}}.ipfs.{{host}}", badRedirectDirCID, u.Host) tests = append(tests, SugarTests{ { Name: "newline: request for $NEWLINE_REDIRECTS_DIR_HOSTNAME/redirect-one redirects with default of 301, per _redirects file", Request: Request(). - URL("{{url}}/redirect-one", newlineBaseURL), + Header("Host", newlineHost). + Path("/redirect-one"), Response: Expect(). Status(301). Headers( @@ -208,7 +214,8 @@ func TestRedirectsFileSupport(t *testing.T) { { Name: "good codes: request for $GOOD_REDIRECTS_DIR_HOSTNAME/redirect-one redirects with default of 301, per _redirects file", Request: Request(). - URL("{{url}}/a301", goodRedirectDirBaseURL), + Header("Host", goodRedirectDirHost). + Path("/a301"), Response: Expect(). Status(301). Headers( @@ -218,7 +225,8 @@ func TestRedirectsFileSupport(t *testing.T) { { Name: "bad codes: request for $BAD_REDIRECTS_DIR_HOSTNAME/found.html doesn't return error about bad code", Request: Request(). - URL("{{url}}/found.html", badRedirectDirBaseURL), + Header("Host", badRedirectDirHost). + Path("/found.html"), Response: Expect(). Status(200). Body( @@ -230,7 +238,7 @@ func TestRedirectsFileSupport(t *testing.T) { }, }...) - RunWithSpecs(t, helpers.UnwrapSubdomainTests(t, tests), specs.SubdomainGatewayIPFS, specs.RedirectsFile) + RunWithSpecs(t, tests, specs.SubdomainGatewayIPFS, specs.RedirectsFile) } func TestRedirectsFileSupportWithDNSLink(t *testing.T) { @@ -280,18 +288,15 @@ func TestRedirectsFileWithIfNoneMatchHeader(t *testing.T) { dnsLinks := dnslink.MustOpenDNSLink("redirects_file/dnslink.yml") dnsLink := dnsLinks.MustGet("redirects-spa") - u, err := url.Parse(SubdomainGatewayURL) - if err != nil { - t.Fatal(err) - } + u := SubdomainGatewayURL() dnslinkAtSubdomainGw := Fmt("{{dnslink}}.ipns.{{host}}", dnslink.InlineDNS(dnsLink), u.Host) var etag string - RunWithSpecs(t, helpers.UnwrapSubdomainTests(t, SugarTests{ + RunWithSpecs(t, SugarTests{ { - Name: "request for //{{dnslink}}.ipns.{{subdomain-gateway}}/missing-page returns body of index.html as per _redirects", + Name: "request for //{dnslink}.ipns.{subdomain-gateway}/missing-page returns body of index.html as per _redirects", Request: Request(). Path("/missing-page"). Headers( @@ -309,11 +314,11 @@ func TestRedirectsFileWithIfNoneMatchHeader(t *testing.T) { ). Body(fixture.MustGetRawData("index.html")), }, - }), specs.SubdomainGatewayIPNS, specs.RedirectsFile) + }, specs.SubdomainGatewayIPNS, specs.RedirectsFile) - RunWithSpecs(t, helpers.UnwrapSubdomainTests(t, SugarTests{ + RunWithSpecs(t, SugarTests{ { - Name: "request for //{dnslink}.ipns.{subdomain-gw}/missing-page with If-None-Match returns 304", + Name: "request for //{dnslink}.ipns.{subdomain-gateway}/missing-page with If-None-Match returns 304", Request: Request(). Path("/missing-page"). Headers( @@ -324,9 +329,9 @@ func TestRedirectsFileWithIfNoneMatchHeader(t *testing.T) { Response: Expect(). Status(304), }, - }), specs.SubdomainGatewayIPNS, specs.RedirectsFile) + }, specs.SubdomainGatewayIPNS, specs.RedirectsFile) - RunWithSpecs(t, helpers.UnwrapSubdomainTests(t, SugarTests{ + RunWithSpecs(t, SugarTests{ { Name: "request for //{dnslink}/missing-page returns body of index.html as per _redirects", Request: Request(). @@ -346,9 +351,9 @@ func TestRedirectsFileWithIfNoneMatchHeader(t *testing.T) { ). Body(fixture.MustGetRawData("index.html")), }, - }), specs.DNSLinkGateway, specs.RedirectsFile) + }, specs.DNSLinkGateway, specs.RedirectsFile) - RunWithSpecs(t, helpers.UnwrapSubdomainTests(t, SugarTests{ + RunWithSpecs(t, SugarTests{ { Name: "request for //{dnslink}/missing-page with If-None-Match returns 304", Request: Request(). @@ -361,5 +366,5 @@ func TestRedirectsFileWithIfNoneMatchHeader(t *testing.T) { Response: Expect(). Status(304), }, - }), specs.DNSLinkGateway, specs.RedirectsFile) + }, specs.DNSLinkGateway, specs.RedirectsFile) } diff --git a/tests/subdomain_gateway_ipfs_test.go b/tests/subdomain_gateway_ipfs_test.go index bca6cbedc..8d86624b6 100644 --- a/tests/subdomain_gateway_ipfs_test.go +++ b/tests/subdomain_gateway_ipfs_test.go @@ -1,15 +1,14 @@ package tests import ( - "net/url" "testing" "github.com/ipfs/gateway-conformance/tooling" "github.com/ipfs/gateway-conformance/tooling/car" . "github.com/ipfs/gateway-conformance/tooling/check" - "github.com/ipfs/gateway-conformance/tooling/helpers" "github.com/ipfs/gateway-conformance/tooling/specs" . "github.com/ipfs/gateway-conformance/tooling/test" + . "github.com/ipfs/gateway-conformance/tooling/tmpl" ) func TestUnixFSDirectoryListingOnSubdomainGateway(t *testing.T) { @@ -22,21 +21,14 @@ func TestUnixFSDirectoryListingOnSubdomainGateway(t *testing.T) { tests := SugarTests{} // run against origins explicitly passed via --subdomain-url - u, err := url.Parse(SubdomainGatewayURL) - if err != nil { - t.Fatal(err) - } + u := SubdomainGatewayURL() tests = append(tests, SugarTests{ { Name: "backlink on root CID should be hidden (TODO: cleanup Kubo-specifics)", Request: Request(). - URL( - "{{scheme}}://{{cid}}.ipfs.{{host}}/", - u.Scheme, - root.Cid(), - u.Host, - ), + Header("Host", Fmt("{{cid}}.ipfs.{{host}}", root.Cid(), u.Host)). + Path("/"), Response: Expect(). BodyWithHint("backlink on root CID should be hidden", And( @@ -47,12 +39,8 @@ func TestUnixFSDirectoryListingOnSubdomainGateway(t *testing.T) { { Name: "redirect dir listing to URL with trailing slash", Request: Request(). - URL( - "{{scheme}}://{{cid}}.ipfs.{{host}}/ą/ę", - u.Scheme, - root.Cid(), - u.Host, - ), + Header("Host", Fmt("{{cid}}.ipfs.{{host}}", root.Cid(), u.Host)). + Path("/ą/ę"), Response: Expect(). Status(301). Headers( @@ -61,12 +49,9 @@ func TestUnixFSDirectoryListingOnSubdomainGateway(t *testing.T) { }, { Name: "Regular dir listing HTML (TODO: cleanup Kubo-specifics)", - Request: Request().URL( - "{{scheme}}://{{cid}}.ipfs.{{host}}/ą/ę/", - u.Scheme, - root.Cid(), - u.Host, - ), + Request: Request(). + Header("Host", Fmt("{{cid}}.ipfs.{{host}}", root.Cid(), u.Host)). + Path("/ą/ę/"), Response: Expect(). Headers( Header("Etag").Contains(`"DirIndex-`), @@ -97,7 +82,7 @@ func TestUnixFSDirectoryListingOnSubdomainGateway(t *testing.T) { }, }...) - RunWithSpecs(t, helpers.UnwrapSubdomainTests(t, tests), specs.SubdomainGatewayIPFS) + RunWithSpecs(t, tests, specs.SubdomainGatewayIPFS) } func TestGatewaySubdomains(t *testing.T) { @@ -119,21 +104,19 @@ func TestGatewaySubdomains(t *testing.T) { tests := SugarTests{} // run against origins explicitly passed via --subdomain-url - gatewayURL := SubdomainGatewayURL - u, err := url.Parse(gatewayURL) - if err != nil { - t.Fatal(err) - } + u := SubdomainGatewayURL() tests = append(tests, SugarTests{ { - Name: "request for example.com/ipfs/{CIDv1} redirects to subdomain", + Name: "request for example.com/ipfs/{cid} redirects to {cid}.ipfs.example.com", Hint: ` - path requests to gateways with subdomain support - should not return payload directly, - but redirect to URL with proper origin isolation + path requests to gateways with subdomain support should not + return payload directly, but redirect to URL with proper + origin isolation `, - Request: Request().URL("{{url}}/ipfs/{{cid}}/", gatewayURL, CIDv1), + Request: Request(). + Header("Host", u.Host). + Path("/ipfs/{{cid}}/", CIDv1), Response: Expect(). Status(301). Headers( @@ -143,8 +126,11 @@ func TestGatewaySubdomains(t *testing.T) { ), }, { - Name: "request for example.com/ipfs/{CIDv1}/{filename with percent encoding} redirects to subdomain", - Request: Request().URL("{{url}}/ipfs/{{cid}}/Portugal%252C+España=Peninsula%20Ibérica.txt", gatewayURL, dirWithPercentEncodedFilenameCID), + Name: "request for example.com/ipfs/{CIDv1}/{filename with percent encoding} redirects to subdomain", + Hint: "the path remainder MUST be preserved", + Request: Request(). + Header("Host", u.Host). + Path("/ipfs/{{cid}}/Portugal%252C+España=Peninsula%20Ibérica.txt", dirWithPercentEncodedFilenameCID), Response: Expect(). Status(301). Headers( @@ -152,13 +138,15 @@ func TestGatewaySubdomains(t *testing.T) { ), }, { - Name: "request for example.com/ipfs/{DirCID} redirects to subdomain", + Name: "request for example.com/ipfs/{DirCID}/ redirects to subdomain", Hint: ` - path requests to gateways with subdomain support - should not return payload directly, - but redirect to URL with proper origin isolation + path requests to gateways with subdomain support should not + return payload directly, but redirect to URL with proper + origin isolation `, - Request: Request().URL("{{url}}/ipfs/{{cid}}/", gatewayURL, DirCID), + Request: Request(). + Header("Host", u.Host). + Path("/ipfs/{{cid}}/", DirCID), Response: Expect(). Status(301). Headers( @@ -168,8 +156,10 @@ func TestGatewaySubdomains(t *testing.T) { ), }, { - Name: "request for example.com/ipfs/{CIDv0} redirects to CIDv1 representation in subdomain", - Request: Request().URL("{{url}}/ipfs/{{cid}}/", gatewayURL, CIDv0), + Name: "request for example.com/ipfs/{CIDv0} redirects to {CIDv1}.ipfs.example.com", + Request: Request(). + Header("Host", u.Host). + Path("/ipfs/{{cid}}/", CIDv0), Response: Expect(). Status(301). Headers( @@ -179,53 +169,67 @@ func TestGatewaySubdomains(t *testing.T) { ), }, { - Name: "request for {CID}.ipfs.example.com should return expected payload", - Request: Request().URL("{{scheme}}://{{cid}}.ipfs.{{host}}", u.Scheme, CIDv1, u.Host), + Name: "request for {CID}.ipfs.example.com should return expected payload", + Request: Request(). + Header("Host", Fmt("{{cid}}.ipfs.{{host}}", CIDv1, u.Host)). + Path("/"), Response: Expect(). Status(200). Body(Contains(CIDVal)), }, { - Name: "request for {CID}.ipfs.example.com/ipfs/{CID} should return HTTP 404", - Hint: "ensure /ipfs/ namespace is not mounted on subdomain", - Request: Request().URL("{{scheme}}://{{cid}}.ipfs.{{host}}/ipfs/{{cid}}", u.Scheme, CIDv1, u.Host), + Name: "request for {CID}.ipfs.example.com/ipfs/{CID} should return HTTP 404", + Hint: "ensure /ipfs/ namespace is not mounted on subdomain", + Request: Request(). + Header("Host", Fmt("{{cid}}.ipfs.{{host}}", CIDv1, u.Host)). + Path("/ipfs/{{cid}}/", CIDv1), Response: Expect(). Status(404), }, { - Name: "request for {CID}.ipfs.example.com/ipfs/file.txt should return data from a file in CID content root", - Hint: "ensure requests to /ipfs/* are not blocked, if content root has such subdirectory", - Request: Request().URL("{{scheme}}://{{cid}}.ipfs.{{host}}/ipfs/file.txt", u.Scheme, DirCID, u.Host), + Name: "request for {CID}.ipfs.example.com/ipfs/file.txt should return data from a file in CID content root", + Hint: "ensure requests to /ipfs/* are not blocked, if content root has such subdirectory", + Request: Request(). + Header("Host", Fmt("{{cid}}.ipfs.{{host}}", DirCID, u.Host)). + Path("/ipfs/file.txt"), Response: Expect(). Status(200). Body(Contains("I am a txt file")), }, { - Name: "valid file and subdirectory paths in directory listing at {cid}.ipfs.example.com", - Hint: "{CID}.ipfs.example.com/sub/dir (Directory Listing)", - Request: Request().URL("{{scheme}}://{{cid}}.ipfs.{{host}}/", u.Scheme, DirCID, u.Host), + Name: "valid file and subdirectory paths in directory listing at {cid}.ipfs.example.com", + Hint: "{CID}.ipfs.example.com (Directory Listing)", + Request: Request(). + Header("Host", Fmt("{{cid}}.ipfs.{{host}}", DirCID, u.Host)). + Path("/"), Response: Expect(). Status(200). Body(And( - // TODO: implement html expectations + // TODO: implement html expectations https://github.com/ipfs/gateway-conformance/issues/21 Contains(`hello`), Contains(`ipfs`), )), }, { - Name: "valid parent directory path in directory listing at {cid}.ipfs.example.com/sub/dir", - Request: Request().URL("{{scheme}}://{{cid}}.ipfs.{{host}}/ipfs/ipns/", u.Scheme, DirCID, u.Host), + Name: "valid parent directory path in directory listing at {cid}.ipfs.example.com/sub/dir", + Hint: "{CID}.ipfs.example.com/ipfs/ipns/ (if exists) should produce a valid directory listing", + Request: Request(). + Header("Host", Fmt("{{cid}}.ipfs.{{host}}", DirCID, u.Host)). + Path("/ipfs/ipns/"), Response: Expect(). Status(200). Body(And( - // TODO: implement html expectations + // TODO: implement html expectations https://github.com/ipfs/gateway-conformance/issues/21 Contains(`..`), Contains(`bar`), )), }, { - Name: "request for deep path resource at {cid}.ipfs.example.com/sub/dir/file", - Request: Request().URL("{{scheme}}://{{cid}}.ipfs.{{host}}/ipfs/ipns/bar", u.Scheme, DirCID, u.Host), + Name: "request for deep path resource at {cid}.ipfs.example.com/sub/dir/file", + Hint: "{CID}.ipfs.example.com/ipfs/ipns/bar (if exists) should return expected file", + Request: Request(). + Header("Host", Fmt("{{cid}}.ipfs.{{host}}", DirCID, u.Host)). + Path("/ipfs/ipns/bar"), Response: Expect(). Status(200). Body(Contains("text-file-content")), @@ -236,7 +240,9 @@ func TestGatewaySubdomains(t *testing.T) { Note 1: we test for sneaky subdir names {cid}.ipfs.example.com/ipfs/ipns/ :^) Note 2: example.com/ipfs/.. present in HTML will be redirected to subdomain, so this is expected behavior `, - Request: Request().URL("{{scheme}}://{{cid}}.ipfs.{{host}}/ipfs/ipns/", u.Scheme, DirCID, u.Host), + Request: Request(). + Header("Host", Fmt("{{cid}}.ipfs.{{host}}", DirCID, u.Host)). + Path("/ipfs/ipns/"), Response: Expect(). Status(200). Body( @@ -248,50 +254,47 @@ func TestGatewaySubdomains(t *testing.T) { ), }, { - Name: "request for example.com/ipfs/{CIDv1} produces redirect to {CIDv1}.ipfs.example.com", - Hint: "path requests to the root hostname should redirect to a subdomain URL with proper origin isolation", - Request: Request().URL("{{scheme}}://{{host}}/ipfs/{{cid}}/", u.Scheme, u.Host, CIDv1), - Response: Expect(). - Headers( - Header("Location").Equals("{{scheme}}://{{cid}}.ipfs.{{host}}/", u.Scheme, CIDv1, u.Host), - ), - }, - { - Name: "request for example.com/ipfs/{InvalidCID} produces useful error before redirect", - Hint: "error message should include original CID (and it should be case-sensitive, as we can't assume everyone uses base32)", - Request: Request().URL("{{scheme}}://{{host}}/ipfs/QmInvalidCID", u.Scheme, u.Host), + Name: "request for example.com/ipfs/{InvalidCID} produces useful error before redirect", + Hint: "error message should include original CID (and it should be case-sensitive, as we can't assume everyone uses base32)", + Request: Request(). + Header("Host", u.Host). + Path("/ipfs/QmInvalidCID"), Response: Expect(). Body(Contains(`invalid path "/ipfs/QmInvalidCID"`)), }, - { - Name: "request for example.com/ipfs/{CIDv0} produces redirect to {CIDv1}.ipfs.example.com", - Request: Request().URL("{{scheme}}://{{host}}/ipfs/{{cid}}/", u.Scheme, u.Host, CIDv0), + Name: "request for example.com/ipfs/{CID} with X-Forwarded-Proto: https produces redirect to HTTPS URL", + Hint: "Support X-Forwarded-Proto", + Request: Request(). + Header("Host", u.Host). + Header("X-Forwarded-Proto", "https"). + Path("/ipfs/{{cid}}/", CIDv1), Response: Expect(). Status(301). Headers( - Header("Location").Equals("{{scheme}}://{{cid}}.ipfs.{{host}}/", u.Scheme, CIDv0to1, u.Host), + Header("Location").Equals("https://{{cid}}.ipfs.{{host}}/", CIDv1, u.Host), ), }, - { - Name: "request for http://example.com/ipfs/{CID} with X-Forwarded-Proto: https produces redirect to HTTPS URL", + Name: "request for example.com/ipfs/{CID} with X-Forwarded-Proto: http produces redirect to HTTP URL", Hint: "Support X-Forwarded-Proto", - Request: Request().URL("{{scheme}}://{{host}}/ipfs/{{cid}}/", u.Scheme, u.Host, CIDv1). - Header("X-Forwarded-Proto", "https"), + Request: Request(). + Header("Host", u.Host). + Header("X-Forwarded-Proto", "http"). + Path("/ipfs/{{cid}}/", CIDv1), Response: Expect(). Status(301). Headers( - Header("Location").Equals("https://{{cid}}.ipfs.{{host}}/", CIDv1, u.Host), + Header("Location").Equals("http://{{cid}}.ipfs.{{host}}/", CIDv1, u.Host), ), }, { Name: "request for example.com/ipfs/?uri=ipfs%3A%2F%2F.. produces redirect to /ipfs/.. content path", Hint: "Support ipfs:// in https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler", - Request: Request().URL("{{scheme}}://{{host}}/ipfs/", u.Scheme, u.Host). - Query( - "uri", "ipfs://{{host}}/wiki/Diego_Maradona.html", CIDWikipedia, - ), + Request: Request(). + Header("Host", u.Host). + Path("/ipfs/"). + Query("uri", "ipfs://{{host}}/wiki/Diego_Maradona.html", CIDWikipedia), Response: Expect(). Status(301). Headers( @@ -299,17 +302,21 @@ func TestGatewaySubdomains(t *testing.T) { ), }, { - Name: "request for a too long CID at example.com/ipfs/{CIDv1} returns human readable error", - Hint: "router should not redirect to hostnames that could fail due to DNS limits", - Request: Request().URL("{{url}}/ipfs/{{cid}}", gatewayURL, CIDv1_TOO_LONG), + Name: "request for a too long CID at example.com/ipfs/{CIDv1} returns human readable error", + Hint: "router should not redirect to hostnames that could fail due to DNS limits", + Request: Request(). + Header("Host", u.Host). + Path("/ipfs/{{cid}}/", CIDv1_TOO_LONG), Response: Expect(). Status(400). Body(Contains("CID incompatible with DNS label length limit of 63")), }, { - Name: "request for a too long CID at {CIDv1}.ipfs.example.com returns expected payload", - Hint: "direct request should also fail (provides the same UX as router and avoids confusion)", - Request: Request().URL("{{scheme}}://{{cid}}.ipfs.{{host}}/", u.Scheme, CIDv1_TOO_LONG, u.Host), + Name: "request for a too long CID at {CIDv1}.ipfs.example.com returns expected payload", + Hint: "direct request should also fail (provides the same UX as router and avoids confusion)", + Request: Request(). + Header("Host", Fmt("{{cid}}.ipfs.{{host}}", CIDv1_TOO_LONG, u.Host)). + Path("/"), Response: Expect(). Status(400). Body(Contains("CID incompatible with DNS label length limit of 63")), @@ -318,17 +325,21 @@ func TestGatewaySubdomains(t *testing.T) { // ## Test support for X-Forwarded-Host // ## ============================================================================ { - Name: "request for http://fake.domain.com/ipfs/{CID} doesn't match the example.com gateway", + Name: "request for fake.domain.com/ipfs/{CID} doesn't match the example.com gateway", + Hint: "when there is no Host match, request is processed as a path gateway", Request: Request(). - URL("{{scheme}}://{{domain}}/ipfs/{{cid}}", u.Scheme, "fake.domain.com", CIDv1), + Header("Host", "fake.domain.com"). + Path("/ipfs/{{cid}}", CIDv1), Response: Expect(). Status(200), }, { - Name: "request for http://fake.domain.com/ipfs/{CID} with X-Forwarded-Host: example.com match the example.com gateway", + Name: "request for fake.domain.com/ipfs/{CID} with X-Forwarded-Host: example.com match the example.com gateway", + Hint: "X-Forwarded-Host overrides Host, request should be processed as a subdomain gateway", Request: Request(). - URL("{{scheme}}://{{domain}}/ipfs/{{cid}}", u.Scheme, "fake.domain.com", CIDv1). - Header("X-Forwarded-Host", u.Host), + Header("Host", "fake.domain.com"). + Header("X-Forwarded-Host", u.Host). + Path("/ipfs/{{cid}}", CIDv1), Response: Expect(). Status(301). Headers( @@ -336,9 +347,10 @@ func TestGatewaySubdomains(t *testing.T) { ), }, { - Name: "request for http://fake.domain.com/ipfs/{CID} with X-Forwarded-Host: example.com and X-Forwarded-Proto: https match the example.com gateway, redirect with https", + Name: "request for fake.domain.com/ipfs/{CID} with X-Forwarded-Host: example.com and X-Forwarded-Proto: https match the example.com gateway, redirect with https", Request: Request(). - URL("{{scheme}}://{{domain}}/ipfs/{{cid}}", u.Scheme, "fake.domain.com", CIDv1). + Header("Host", "fake.domain.com"). + Path("/ipfs/{{cid}}", CIDv1). Header("X-Forwarded-Host", u.Host). Header("X-Forwarded-Proto", "https"), Response: Expect(). @@ -347,7 +359,20 @@ func TestGatewaySubdomains(t *testing.T) { Header("Location").Equals("https://{{cid}}.ipfs.{{host}}/", CIDv1, u.Host), ), }, + { + Name: "request for fake.domain.com/ipfs/{CID} with X-Forwarded-Host: example.com and X-Forwarded-Proto: http match the example.com gateway, redirect with http", + Request: Request(). + Header("Host", "fake.domain.com"). + Path("/ipfs/{{cid}}", CIDv1). + Header("X-Forwarded-Host", u.Host). + Header("X-Forwarded-Proto", "http"), + Response: Expect(). + Status(301). + Headers( + Header("Location").Equals("http://{{cid}}.ipfs.{{host}}/", CIDv1, u.Host), + ), + }, }...) - RunWithSpecs(t, helpers.UnwrapSubdomainTests(t, tests), specs.SubdomainGatewayIPFS) + RunWithSpecs(t, tests, specs.SubdomainGatewayIPFS) } diff --git a/tests/subdomain_gateway_ipns_test.go b/tests/subdomain_gateway_ipns_test.go index 74c4ce769..997595a93 100644 --- a/tests/subdomain_gateway_ipns_test.go +++ b/tests/subdomain_gateway_ipns_test.go @@ -1,16 +1,15 @@ package tests import ( - "net/url" "testing" "github.com/ipfs/gateway-conformance/tooling" "github.com/ipfs/gateway-conformance/tooling/car" "github.com/ipfs/gateway-conformance/tooling/dnslink" - "github.com/ipfs/gateway-conformance/tooling/helpers" "github.com/ipfs/gateway-conformance/tooling/ipns" "github.com/ipfs/gateway-conformance/tooling/specs" . "github.com/ipfs/gateway-conformance/tooling/test" + . "github.com/ipfs/gateway-conformance/tooling/tmpl" "github.com/multiformats/go-multibase" "github.com/multiformats/go-multicodec" ) @@ -32,18 +31,27 @@ func TestGatewaySubdomainAndIPNS(t *testing.T) { } // run against origins passed via --subdomain-url (e.g. http://localhost:port) - gatewayURL := SubdomainGatewayURL - u, err := url.Parse(gatewayURL) - if err != nil { - t.Fatal(err) - } + u := SubdomainGatewayURL() for _, record := range ipnsRecords { tests = append(tests, SugarTests{ { Name: "request for /ipns/{CIDv0} redirects to CIDv1 with libp2p-key multicodec in subdomain", Request: Request(). - URL("{{url}}/ipns/{{cid}}", gatewayURL, record.IdV0()), + Header("Host", u.Host). + Path("/ipns/{{id}}", record.IdV0()), + Response: Expect(). + Status(301). + Headers( + Header("Location"). + Equals("{{scheme}}://{{cid}}.ipns.{{host}}/", u.Scheme, record.IdV1(), u.Host), + ), + }, + { + Name: "request for /ipns/{CIDv1} redirects to same CIDv1 on subdomain", + Request: Request(). + Header("Host", u.Host). + Path("/ipns/{{id}}", record.IdV1()), Response: Expect(). Status(301). Headers( @@ -52,9 +60,10 @@ func TestGatewaySubdomainAndIPNS(t *testing.T) { ), }, { - Name: "request for {CIDv1-libp2p-key}.ipns.{gateway} returns expected payload", + Name: "request for {CIDv1-base36-libp2p-key}.ipns.{gateway} returns expected payload", Request: Request(). - URL("{{scheme}}://{{cid}}.ipns.{{host}}/", u.Scheme, record.IdV1(), u.Host), + Header("Host", Fmt("{{cid}}.ipns.{{host}}", record.IdV1(), u.Host)). + Path("/"), Response: Expect(). Status(200). BodyWithHint("Request for {{cid}}.ipns.{{host}} returns expected payload", payload), @@ -62,7 +71,8 @@ func TestGatewaySubdomainAndIPNS(t *testing.T) { { Name: "request for {CIDv1-dag-pb}.ipns.{gateway} redirects to CID with libp2p-key multicodec", Request: Request(). - URL("{{scheme}}://{{cid}}.ipns.{{host}}/", u.Scheme, record.ToCID(multicodec.DagPb, multibase.Base36), u.Host), + Header("Host", Fmt("{{cid}}.ipns.{{host}}", record.ToCID(multicodec.DagPb, multibase.Base36), u.Host)). + Path("/"), Response: Expect(). Status(301). Headers( @@ -142,7 +152,8 @@ func TestGatewaySubdomainAndIPNS(t *testing.T) { { Name: "request for a ED25519 libp2p-key at example.com/ipns/{b58mh} returns Location HTTP header for DNS-safe subdomain redirect in browsers", Request: Request(). - URL("{{url}}/ipns/{{cid}}", gatewayURL, ed25519Fixture.B58MH()), + Header("Host", u.Host). + Path("/ipns/{{b58mh}}", ed25519Fixture.B58MH()), Response: Expect(). Headers( Header("Location"). @@ -151,7 +162,7 @@ func TestGatewaySubdomainAndIPNS(t *testing.T) { }, }...) - RunWithSpecs(t, helpers.UnwrapSubdomainTests(t, tests), specs.SubdomainGatewayIPNS) + RunWithSpecs(t, tests, specs.SubdomainGatewayIPNS) } func TestSubdomainGatewayDNSLinkInlining(t *testing.T) { @@ -164,27 +175,34 @@ func TestSubdomainGatewayDNSLinkInlining(t *testing.T) { dnsLinkTest := dnsLinks.MustGet("test") // run against origins passed via --subdomain-url - gatewayURL := SubdomainGatewayURL - u, err := url.Parse(gatewayURL) - if err != nil { - t.Fatal(err) - } + u := SubdomainGatewayURL() tests = append(tests, SugarTests{ { - Name: "request for /ipns/{fqdn} redirects to DNSLink in subdomain", + Name: "request for /ipns/{dnslink}/foo/ redirects to {inlined-dnslink}.ipns.example.com", + Hint: "https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header", Request: Request(). - URL("{{url}}/ipns/{{fqdn}}/wiki/", gatewayURL, wikipedia), + Header("Host", u.Host). + Path("/ipns/{{dnslink}}/wiki/", wikipedia), Response: Expect(). Headers( Header("Location"). - Equals("{{scheme}}://{{fqdn}}.ipns.{{host}}/wiki/", u.Scheme, dnslink.InlineDNS(wikipedia), u.Host), + Equals("{{scheme}}://{{inlined}}.ipns.{{host}}/wiki/", u.Scheme, dnslink.InlineDNS(wikipedia), u.Host), ), }, { Name: "request for {dnslink}.ipns.{gateway} returns expected payload", Request: Request(). - URL("{{scheme}}://{{fqdn}}.ipns.{{host}}", u.Scheme, dnsLinkTest, u.Host), + Header("Host", Fmt("{{dnslink}}.ipns.{{host}}", dnsLinkTest, u.Host)). + Path("/"), + Response: Expect(). + Body("hello\n"), + }, + { + Name: "request for {inlineddnslink}.ipns.{gateway} returns expected payload", + Request: Request(). + Header("Host", Fmt("{{inlined}}.ipns.{{host}}", dnslink.InlineDNS(dnsLinkTest), u.Host)). + Path("/"), Response: Expect(). Body("hello\n"), }, @@ -196,7 +214,8 @@ func TestSubdomainGatewayDNSLinkInlining(t *testing.T) { `, Request: Request(). Header("X-Forwarded-Proto", "https"). - URL("{{url}}/ipns/{{wikipedia}}/wiki/", gatewayURL, wikipedia), + Header("Host", u.Host). + Path("/ipns/{{wikipedia}}/wiki/", wikipedia), Response: Expect(). Headers( Header("Location"). @@ -207,8 +226,9 @@ func TestSubdomainGatewayDNSLinkInlining(t *testing.T) { Name: `request for example.com/ipns/?uri=ipns%3A%2F%2F.. produces redirect to /ipns/.. content path`, Hint: "Support ipns:// in https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler", Request: Request(). - // TODO: use Query or future QueryRaw here - URL(`{{url}}/ipns/?uri=ipns%3A%2F%2F{{dnslink}}`, gatewayURL, wikipedia), + Header("Host", u.Host). + Path("/ipns/"). + Query("uri", "ipns://{{dnslink}}", wikipedia), Response: Expect(). Headers( Header("Location").Equals("/ipns/{{wikipedia}}", wikipedia), @@ -216,5 +236,5 @@ func TestSubdomainGatewayDNSLinkInlining(t *testing.T) { }, }...) - RunWithSpecs(t, helpers.UnwrapSubdomainTests(t, tests), specs.SubdomainGatewayIPNS) + RunWithSpecs(t, tests, specs.SubdomainGatewayIPNS) } diff --git a/tests/subdomain_gateway_proxy_test.go b/tests/subdomain_gateway_proxy_test.go new file mode 100644 index 000000000..4e1f7c348 --- /dev/null +++ b/tests/subdomain_gateway_proxy_test.go @@ -0,0 +1,141 @@ +package tests + +import ( + "testing" + + "github.com/ipfs/gateway-conformance/tooling/car" + . "github.com/ipfs/gateway-conformance/tooling/check" + "github.com/ipfs/gateway-conformance/tooling/specs" + . "github.com/ipfs/gateway-conformance/tooling/test" + . "github.com/ipfs/gateway-conformance/tooling/tmpl" +) + +var ( + fixture = car.MustOpenUnixfsCar("subdomain_gateway/fixtures.car") + + CIDVal = string(fixture.MustGetRawData("hello-CIDv1")) // hello + DirCID = fixture.MustGetCid("testdirlisting") + CIDv1 = fixture.MustGetCid("hello-CIDv1") + CIDv0 = fixture.MustGetCid("hello-CIDv0") + CIDv0to1 = fixture.MustGetCid("hello-CIDv0to1") + //CIDv1_TOO_LONG = fixture.MustGetCid("hello-CIDv1_TOO_LONG") + + // the gateway endpoint is used as HTTP proxy + gatewayAsProxyURL = GatewayURL().String() + + // run against origins explicitly passed via --subdomain-url + s = SubdomainGatewayURL() +) + +func TestProxyGatewaySubdomains(t *testing.T) { + tests := SugarTests{ + { + Name: "request for {CID}.ipfs.example.com should return expected payload", + Hint: "HTTP proxy gateway accepts requests for GETs of full URLs as Paths", + Request: Request(). + Proxy(gatewayAsProxyURL). + Path("{{scheme}}://{{cid}}.ipfs.{{host}}", s.Scheme, CIDv1, s.Host), + Response: Expect(). + Status(200). + Body(Contains(CIDVal)), + }, + { + Name: "request for example.com/ipfs/{CIDv0} redirects to {CIDv1}.ipfs.example.com", + Hint: "HTTP proxy gateway accepts requests for GETs of full URLs as Paths", + Request: Request(). + Proxy(gatewayAsProxyURL). + Path("{{scheme}}://{{host}}/ipfs/{{cid}}/", s.Scheme, s.Host, CIDv0), + Response: Expect(). + Status(301). + Headers( + Header("Location"). + Hint("request for example.com/ipfs/{CIDv0to1} returns Location HTTP header for subdomain redirect in browsers"). + Contains("{{scheme}}://{{cid}}.ipfs.{{host}}/", s.Scheme, CIDv0to1, s.Host), + ), + }, + { + Name: "request for {CID}.ipfs.example.com/ipfs/file.txt should return data from a file in CID content root", + Hint: "ensure subdomain gateway takes priority over processing /ipfs/* paths", + Request: Request(). + Proxy(gatewayAsProxyURL). + Path("{{scheme}}://{{cid}}.ipfs.{{host}}/ipfs/file.txt", s.Scheme, DirCID, s.Host), + Response: Expect(). + Status(200). + Body(Contains("I am a txt file")), + }, + /* TODO: value added + { + Name: "request for a too long CID at {CIDv1}.ipfs.example.com returns expected payload", + Hint: "HTTP proxy mode allows responding to requests with 'DNS labels' longer than 63 characters", + Request: Request(). + Proxy(gatewayAsProxyURL). + TODO turn to Path: Header("Host", Fmt("{{cid}}.ipfs.{{host}}", CIDv1_TOO_LONG, s.Host)). + Path("/"), + Response: Expect(). + Status(400). + Body(Contains("TODO")), + }, + */ + } + RunWithSpecs(t, tests, specs.ProxyGateway, specs.SubdomainGatewayIPFS) +} + +func TestProxyTunnelGatewaySubdomains(t *testing.T) { + tests := SugarTests{ + { + Name: "request for {CID}.ipfs.example.com should return expected payload", + Hint: "HTTP CONNECT is how some proxy setups convert an HTTP connection into a tunnel to a remote host https://tools.ietf.org/html/rfc7231#section-4.3.6", + Request: Request(). + WithProxyTunnel(). + Proxy(gatewayAsProxyURL). + Header("Host", Fmt("{{cid}}.ipfs.{{host}}", CIDv1, s.Host)). + Path("/"), + Response: Expect(). + Status(200). + Body(Contains(CIDVal)), + }, + { + Name: "request for example.com/ipfs/{CIDv0} redirects to {CIDv1}.ipfs.example.com", + Hint: "proxy tunnel follows ", + Request: Request(). + WithProxyTunnel(). + Proxy(gatewayAsProxyURL). + Header("Host", s.Host). + Path("/ipfs/{{cid}}/", CIDv0), + Response: Expect(). + Status(301). + Headers( + Header("Location"). + Hint("request for example.com/ipfs/{CIDv0to1} returns Location HTTP header for subdomain redirect in browsers"). + Contains("{{scheme}}://{{cid}}.ipfs.{{host}}/", s.Scheme, CIDv0to1, s.Host), + ), + }, + { + Name: "request for {CID}.ipfs.example.com/ipfs/file.txt should return data from a file in CID content root", + Hint: "ensure subdomain gateway takes priority over processing /ipfs/* paths", + Request: Request(). + WithProxyTunnel(). + Proxy(gatewayAsProxyURL). + Header("Host", Fmt("{{cid}}.ipfs.{{host}}", DirCID, s.Host)). + Path("/ipfs/file.txt"), + Response: Expect(). + Status(200). + Body(Contains("I am a txt file")), + }, + /* TODO: value added + { + Name: "request for a too long CID at {CIDv1}.ipfs.example.com returns expected payload", + Hint: "HTTP proxy mode allows responding to requests with 'DNS labels' longer than 63 characters", + Request: Request(). + WithProxyTunnel(). + Proxy(gatewayAsProxyURL). + TODO turn to Path: Header("Host", Fmt("{{cid}}.ipfs.{{host}}", CIDv1_TOO_LONG, s.Host)). + Path("/"), + Response: Expect(). + Status(400). + Body(Contains("TODO")), + }, + */ + } + RunWithSpecs(t, tests, specs.ProxyGateway, specs.SubdomainGatewayIPFS) +} diff --git a/tooling/helpers/subdomain.go b/tooling/helpers/subdomain.go deleted file mode 100644 index b1850f22e..000000000 --- a/tooling/helpers/subdomain.go +++ /dev/null @@ -1,106 +0,0 @@ -package helpers - -import ( - "fmt" - "net/url" - "testing" - - "github.com/ipfs/gateway-conformance/tooling/test" -) - -/** - * UnwrapSubdomainTests takes a list of tests and returns a (larger) list of tests - * that will run on the subdomain gateway. - */ -func UnwrapSubdomainTests(t *testing.T, tests test.SugarTests) test.SugarTests { - t.Helper() - - var out test.SugarTests - for _, test := range tests { - out = append(out, unwrapSubdomainTest(t, test)...) - } - return out -} - -func unwrapSubdomainTest(t *testing.T, unwraped test.SugarTest) test.SugarTests { - t.Helper() - - var logicalURL, httpEndpointURL string - req := unwraped.Request - expected := unwraped.Response - host := req.RemoveHeader("Host") - if host != "" { - // when custom Host header is present we skip legacy magic - // and use Host and Path as-is - u, err := url.Parse(test.GatewayURL) - if err != nil { - panic("failed to parse GatewayURL") - } - u.Host = host - // httpEndpointURL is gateway-url + Path - u.Path = unwraped.Request.Path_ - unwraped.Request.Path_ = "" - httpEndpointURL = u.String() - // logicalURL is httpEndpointURL with hostname from Host header - logicalURL = u.String() - unwraped.Request.URL_ = logicalURL - } else { - // Legacy flow based on URL instead of Host header - logicalURL := unwraped.Request.GetURL() - - u, err := url.Parse(logicalURL) - if err != nil { - t.Fatal(err) - } - - // change the low level HTTP endpoint to one defined via --gateway-url - // to allow testing Host-based logic against arbitrary gateway URL (useful on CI) - u.Host = test.GatewayHost - - httpEndpointURL = u.String() - } - - // TODO: we want to refactor this magic into explicit Proxy test suite. - // Having this magic here silently modifies headers such as Host, and if a - // test fails, it is difficult to grasp how much really is broken, because - // number of errors is always multiplied x3. We should have standalone - // proxy test for subdomain gateway and dnslink (simple GET should be - // enough) and remove need for UnwrapSubdomainTests. - - return test.SugarTests{ - { - Name: fmt.Sprintf("%s (direct HTTP)", unwraped.Name), - Hint: fmt.Sprintf("%s\n%s", unwraped.Hint, "direct HTTP request (hostname in URL, raw IP in Host header)"), - Request: req. - URL(httpEndpointURL). - Headers( - test.Header("Host", host), - ), - Response: expected, - }, - { - Name: fmt.Sprintf("%s (HTTP proxy)", unwraped.Name), - Hint: fmt.Sprintf("%s\n%s", unwraped.Hint, "HTTP proxy (hostname is passed via URL)"), - Request: req. - URL(logicalURL). - Proxy(test.GatewayURL), - Response: expected, - }, - { - Name: fmt.Sprintf("%s (HTTP proxy tunneling via CONNECT)", unwraped.Name), - Hint: fmt.Sprintf("%s\n%s", unwraped.Hint, `HTTP proxy - In HTTP/1.x, the pseudo-method CONNECT, - can be used to convert an HTTP connection into a tunnel to a remote host - https://tools.ietf.org/html/rfc7231#section-4.3.6 - `), - Request: req. - URL(logicalURL). - Proxy(test.GatewayURL). - WithProxyTunnel(). - Headers( - test.Header("Host", host), - ), - Response: expected, - }, - } -} diff --git a/tooling/specs/specs.go b/tooling/specs/specs.go index af4d136e8..f99bd7d37 100644 --- a/tooling/specs/specs.go +++ b/tooling/specs/specs.go @@ -121,6 +121,7 @@ var ( SubdomainGateway = Collection{"subdomain-gateway", []Spec{SubdomainGatewayIPFS, SubdomainGatewayIPNS}} DNSLinkGateway = Leaf{"dnslink-gateway", stable} RedirectsFile = Leaf{"redirects-file", stable} + ProxyGateway = Leaf{"proxy-gateway", stable} ) // All specs MUST be listed here. @@ -141,6 +142,7 @@ var specs = []Spec{ SubdomainGateway, DNSLinkGateway, RedirectsFile, + ProxyGateway, } var specEnabled = map[Spec]bool{} diff --git a/tooling/test/config.go b/tooling/test/config.go index 3d501da83..ef94a7d59 100644 --- a/tooling/test/config.go +++ b/tooling/test/config.go @@ -10,44 +10,23 @@ import ( var log = logging.Logger("conformance") -func GetEnv(key string, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value +func env2url(key string) *url.URL { + value, ok := os.LookupEnv(key) + if !ok { + panic(key + " must be set") } - return fallback -} - -var GatewayURL = strings.TrimRight( - GetEnv("GATEWAY_URL", "http://127.0.0.1:8080"), - "/") - -var SubdomainGatewayURL = strings.TrimRight( - GetEnv("SUBDOMAIN_GATEWAY_URL", "http://localhost:8080"), - "/") - -var GatewayHost = "" -var SubdomainGatewayHost = "" -var SubdomainGatewayScheme = "" - -func init() { - parsed, err := url.Parse(GatewayURL) + gatewayURL := strings.TrimRight(value, "/") + parsed, err := url.Parse(gatewayURL) if err != nil { panic(err) } + return parsed +} - GatewayHost = parsed.Host - - parsed, err = url.Parse(SubdomainGatewayURL) - if err != nil { - panic(err) - } - - SubdomainGatewayHost = parsed.Host - SubdomainGatewayScheme = parsed.Scheme - - log.Debugf("GatewayURL: %s", GatewayURL) +func GatewayURL() *url.URL { + return env2url("GATEWAY_URL") +} - log.Debugf("SubdomainGatewayURL: %s", SubdomainGatewayURL) - log.Debugf("SubdomainGatewayHost: %s", SubdomainGatewayHost) - log.Debugf("SubdomainGatewayScheme: %s", SubdomainGatewayScheme) +func SubdomainGatewayURL() *url.URL { + return env2url("SUBDOMAIN_GATEWAY_URL") } diff --git a/tooling/test/proxy.go b/tooling/test/proxy.go index 55e893e3e..d1888f68f 100644 --- a/tooling/test/proxy.go +++ b/tooling/test/proxy.go @@ -10,6 +10,8 @@ import ( "net/url" ) +// NewProxyTunnelClient creates an HTTP client that routes requests through an HTTP proxy +// using the CONNECT method, as described in RFC 7231 Section 4.3.6. func NewProxyTunnelClient(proxyURL string) *http.Client { proxy, err := url.Parse(proxyURL) if err != nil { @@ -52,6 +54,7 @@ func NewProxyTunnelClient(proxyURL string) *http.Client { return conn, nil }, + // Skip TLS cert verification to make it easier to test on CI and dev envs TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } @@ -62,6 +65,7 @@ func NewProxyTunnelClient(proxyURL string) *http.Client { return client } +// NewProxyClient creates an HTTP client that routes requests through an HTTP proxy. func NewProxyClient(proxyURL string) *http.Client { proxy, err := url.Parse(proxyURL) if err != nil { diff --git a/tooling/test/run.go b/tooling/test/run.go index 8e6e91dea..d408ef270 100644 --- a/tooling/test/run.go +++ b/tooling/test/run.go @@ -6,7 +6,10 @@ import ( "fmt" "io" "net/http" + "strings" "testing" + + "github.com/ipfs/gateway-conformance/tooling" ) type Reporter func(t *testing.T, msg interface{}, rest ...interface{}) @@ -18,8 +21,9 @@ func runRequest(ctx context.Context, t *testing.T, test SugarTest, builder Reque } // Prepare a client, - // use proxy, deal with redirects, etc. client := &http.Client{} + + // HTTP proxy tests require additional prep if builder.UseProxyTunnel_ { if builder.Proxy_ == "" { t.Fatal("ProxyTunnel requires a proxy") @@ -30,6 +34,7 @@ func runRequest(ctx context.Context, t *testing.T, test SugarTest, builder Reque client = NewProxyClient(builder.Proxy_) } + // Handle redirect tests if !builder.FollowRedirects_ { client.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse @@ -54,21 +59,26 @@ func runRequest(ctx context.Context, t *testing.T, test SugarTest, builder Reque } var url string - if builder.URL_ != "" && builder.Path_ != "" { - localReport(t, "Both 'URL' and 'Path' are set") - } - if builder.URL_ == "" && builder.Path_ == "" { - localReport(t, "Neither 'URL' nor 'Path' are set") - } - if builder.URL_ != "" { - url = builder.URL_ + if builder.Path_ == "" { + localReport(t, "'Path' is not set") } if builder.Path_ != "" { - if builder.Path_[0] != '/' { - localReport(t, "Path must start with '/'") + if builder.Proxy_ != "" && !builder.UseProxyTunnel_ { + // plain HTTP proxy test uses custom client, and the Path is the full URL + // to be used in the request + if !strings.HasPrefix(builder.Path_, "http") { + t.Fatalf("plain Proxy tests require requested Path to be full URL starting with http. builder.Path_ was %q", builder.Path_) + } + // plain proxy requests use Path as-is + url = builder.Path_ + } else { + // no HTTP proxy, make a regular HTTP request for Path at GatewayURL (+ optional Host header) + if builder.Path_[0] != '/' { + localReport(t, "When proxy mode is not used, the Path must start with '/'") + } + // regular requests attach Path to gateway endpoint URL + url = fmt.Sprintf("%s%s", strings.TrimRight(GatewayURL().String(), "/"), builder.Path_) } - - url = fmt.Sprintf("%s%s", GatewayURL, builder.Path_) } query := builder.Query_.Encode() @@ -97,7 +107,12 @@ func runRequest(ctx context.Context, t *testing.T, test SugarTest, builder Reque } } - // send request + // Set meaningful User-Agent, if custom one was not set by a test + if _, exists := builder.Headers_["User-Agent"]; !exists { + req.Header.Set("User-Agent", "ipfs/gateway-conformance/"+tooling.Version) + } + + // Send request log.Debugf("Querying %s", url) req = req.WithContext(ctx) diff --git a/tooling/test/sugar.go b/tooling/test/sugar.go index 07a0f023d..cbe0228da 100644 --- a/tooling/test/sugar.go +++ b/tooling/test/sugar.go @@ -14,7 +14,6 @@ import ( type RequestBuilder struct { Method_ string `json:"method,omitempty"` Path_ string `json:"path,omitempty"` - URL_ string `json:"url,omitempty"` Proxy_ string `json:"proxy,omitempty"` UseProxyTunnel_ bool `json:"useProxyTunnel,omitempty"` Headers_ map[string]string `json:"headers,omitempty"` @@ -40,26 +39,11 @@ func (r RequestBuilder) Path(path string, args ...any) RequestBuilder { return r } -func (r RequestBuilder) URL(path string, args ...any) RequestBuilder { - r.URL_ = tmpl.Fmt(path, args...) - return r -} - func (r RequestBuilder) Query(key, value string, args ...any) RequestBuilder { r.Query_.Add(key, tmpl.Fmt(value, args...)) return r } -func (r RequestBuilder) GetURL() string { - if r.Path_ != "" { - // This seems to be some tech debt. Generally, we want to move away from URL, - // and instead just provide Path + Host header - panic("calling GetURL() is not supported when Path is set") - } - - return r.URL_ -} - func (r RequestBuilder) RemoveHeader(hdr string) string { if r.Headers_ != nil { v, ok := r.Headers_[hdr] @@ -138,7 +122,6 @@ func (r RequestBuilder) Clone() RequestBuilder { return RequestBuilder{ Method_: r.Method_, Path_: r.Path_, - URL_: r.URL_, Proxy_: r.Proxy_, UseProxyTunnel_: r.UseProxyTunnel_, Headers_: clonedHeaders,