diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e0c9b66e0..fdf4006a4 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -37,8 +37,7 @@ jobs:
- name: Configure Kubo Gateway
run: |
ipfs init;
- source ./gateway-conformance/kubo-config.example.sh;
- IPFS_NS_MAP=$(cat ./fixtures/dnslinks.json | jq -r 'to_entries | map("\(.key).example.com:\(.value)") | join(",")')
+ source ./gateway-conformance/kubo-config.example.sh "$(pwd)/fixtures"
echo "IPFS_NS_MAP=${IPFS_NS_MAP}" >> $GITHUB_ENV
# note: the IPFS_NS_MAP set above will be passed the daemon
- uses: ipfs/start-ipfs-daemon-action@v1
diff --git a/fixtures/fixture.schema.json b/fixtures/fixture.schema.json
index ae26a75bf..4b5f50476 100644
--- a/fixtures/fixture.schema.json
+++ b/fixtures/fixture.schema.json
@@ -7,6 +7,9 @@
"additionalProperties": {
"type": "object",
"properties": {
+ "domain": {
+ "type": "string"
+ },
"subdomain": {
"type": "string"
},
@@ -15,9 +18,20 @@
}
},
"required": [
- "subdomain",
"path"
],
+ "oneOf": [
+ {
+ "required": [
+ "domain"
+ ]
+ },
+ {
+ "required": [
+ "subdomain"
+ ]
+ }
+ ],
"additionalProperties": false
}
}
diff --git a/fixtures/t0114/12D3KooWLQzUv2FHWGVPXTXSZpdHs7oHbXub2G5WC8Tx4NQhyd2d.ipns-record b/fixtures/t0114/12D3KooWLQzUv2FHWGVPXTXSZpdHs7oHbXub2G5WC8Tx4NQhyd2d.ipns-record
new file mode 100644
index 000000000..39b2f41a4
Binary files /dev/null and b/fixtures/t0114/12D3KooWLQzUv2FHWGVPXTXSZpdHs7oHbXub2G5WC8Tx4NQhyd2d.ipns-record differ
diff --git a/fixtures/t0114/QmVujd5Vb7moysJj8itnGufN7MEtPRCNHkKpNuA4onsRa3.ipns-record b/fixtures/t0114/QmVujd5Vb7moysJj8itnGufN7MEtPRCNHkKpNuA4onsRa3.ipns-record
new file mode 100644
index 000000000..b37d9b75b
Binary files /dev/null and b/fixtures/t0114/QmVujd5Vb7moysJj8itnGufN7MEtPRCNHkKpNuA4onsRa3.ipns-record differ
diff --git a/fixtures/t0114/README.md b/fixtures/t0114/README.md
new file mode 100644
index 000000000..005a7f0c5
--- /dev/null
+++ b/fixtures/t0114/README.md
@@ -0,0 +1,111 @@
+# Dataset description/sources
+
+- fixtures.car
+ - raw CARv1
+
+- QmUKd....ipns-record
+ - ipns record, encoded with protocol buffer
+
+- 12D3K....ipns-record
+ - ipns record, encoded with protocol buffer
+
+Generated with:
+
+```sh
+# using ipfs version 0.21.0-dev (03a98280e3e642774776cd3d0435ab53e5dfa867)
+
+# CIDv0to1 is necessary because raw-leaves are enabled by default during
+# "ipfs add" with CIDv1 and disabled with CIDv0
+CID_VAL="hello"
+CIDv1=$(echo $CID_VAL | ipfs add --cid-version 1 -Q)
+CIDv0=$(echo $CID_VAL | ipfs add --cid-version 0 -Q)
+CIDv0to1=$(echo "$CIDv0" | ipfs cid base32)
+# sha512 will be over 63char limit, even when represented in Base36
+CIDv1_TOO_LONG=$(echo $CID_VAL | ipfs add --cid-version 1 --hash sha2-512 -Q)
+
+echo CID_VAL=${CID_VAL}
+echo CIDv1=${CIDv1}
+echo CIDv0=${CIDv0}
+echo CIDv0to1=${CIDv0to1}
+echo CIDv1_TOO_LONG=${CIDv1_TOO_LONG}
+
+# Directory tree crafted to test for edge cases like "/ipfs/ipfs/ipns/bar"
+mkdir -p testdirlisting/ipfs/ipns &&
+echo "hello" > testdirlisting/hello &&
+echo "text-file-content" > testdirlisting/ipfs/ipns/bar &&
+mkdir -p testdirlisting/api &&
+mkdir -p testdirlisting/ipfs &&
+echo "I am a txt file" > testdirlisting/api/file.txt &&
+echo "I am a txt file" > testdirlisting/ipfs/file.txt &&
+DIR_CID=$(ipfs add -Qr --cid-version 1 testdirlisting)
+
+echo DIR_CID=${DIR_CID} # ./testdirlisting
+
+ipfs files mkdir /t0114/
+ipfs files cp /ipfs/${CIDv1} /t0114/
+ipfs files cp /ipfs/${CIDv0} /t0114/
+ipfs files cp /ipfs/${CIDv0to1} /t0114/
+ipfs files cp /ipfs/${DIR_CID} /t0114/
+ipfs files cp /ipfs/${CIDv1_TOO_LONG} /t0114/
+
+ROOT=`ipfs files stat /t0114/ --hash`
+
+ipfs dag export ${ROOT} > ./fixtures.car
+
+# Then the keys
+
+KEY_NAME=test_key_rsa_$RANDOM
+RSA_KEY=$(ipfs key gen --ipns-base=b58mh --type=rsa --size=2048 ${KEY_NAME} | head -n1 | tr -d "\n")
+RSA_IPNS_IDv0=$(echo "$RSA_KEY" | ipfs cid format -v 0)
+RSA_IPNS_IDv1=$(echo "$RSA_KEY" | ipfs cid format -v 1 --mc libp2p-key -b base36)
+RSA_IPNS_IDv1_DAGPB=$(echo "$RSA_IPNS_IDv0" | ipfs cid format -v 1 -b base36)
+
+# publish a record valid for a 100 years
+ipfs name publish --key ${KEY_NAME} --allow-offline -Q --ttl=876600h --lifetime=876600h "/ipfs/$CIDv1"
+ipfs routing get /ipns/${RSA_KEY} > ${RSA_KEY}.ipns-record
+
+echo RSA_KEY=${RSA_KEY}
+echo RSA_IPNS_IDv0=${RSA_IPNS_IDv0}
+echo RSA_IPNS_IDv1=${RSA_IPNS_IDv1}
+echo RSA_IPNS_IDv1_DAGPB=${RSA_IPNS_IDv1_DAGPB}
+
+KEY_NAME=test_key_ed25519_$RANDOM
+ED25519_KEY=$(ipfs key gen --ipns-base=b58mh --type=ed25519 ${KEY_NAME} | head -n1 | tr -d "\n")
+ED25519_IPNS_IDv0=$ED25519_KEY
+ED25519_IPNS_IDv1=$(ipfs key list -l --ipns-base=base36 | grep ${KEY_NAME} | cut -d " " -f1 | tr -d "\n")
+ED25519_IPNS_IDv1_DAGPB=$(echo "$ED25519_IPNS_IDv1" | ipfs cid format -v 1 -b base36 --mc dag-pb)
+
+# ed25519 fits under 63 char limit when represented in base36
+IPNS_ED25519_B58MH=$(ipfs key list -l --ipns-base b58mh | grep $KEY_NAME | cut -d" " -f1 | tr -d "\n")
+IPNS_ED25519_B36CID=$(ipfs key list -l --ipns-base base36 | grep $KEY_NAME | cut -d" " -f1 | tr -d "\n")
+
+# publish a record valid for a 100 years
+ipfs name publish --key ${KEY_NAME} --allow-offline -Q --ttl=876600h --lifetime=876600h "/ipfs/$CIDv1"
+ipfs routing get /ipns/${ED25519_KEY} > ${ED25519_KEY}.ipns-record
+
+echo ED25519_KEY=${ED25519_KEY}
+echo ED25519_IPNS_IDv0=${ED25519_IPNS_IDv0}
+echo ED25519_IPNS_IDv1=${ED25519_IPNS_IDv1}
+echo ED25519_IPNS_IDv1_DAGPB=${ED25519_IPNS_IDv1_DAGPB}
+echo IPNS_ED25519_B58MH=${IPNS_ED25519_B58MH}
+echo IPNS_ED25519_B36CID=${IPNS_ED25519_B36CID}
+
+# CID_VAL=hello
+# CIDv1=bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am
+# CIDv0=QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN
+# CIDv0to1=bafybeiffndsajwhk3lwjewwdxqntmjm4b5wxaaanokonsggenkbw6slwk4
+# CIDv1_TOO_LONG=bafkrgqhhyivzstcz3hhswshfjgy6ertgmnqeleynhwt4dlfsthi4hn7zgh4uvlsb5xncykzapi3ocd4lzogukir6ksdy6wzrnz6ohnv4aglcs
+# DIR_CID=bafybeiht6dtwk3les7vqm6ibpvz6qpohidvlshsfyr7l5mpysdw2vmbbhe # ./testdirlisting
+
+# RSA_KEY=QmVujd5Vb7moysJj8itnGufN7MEtPRCNHkKpNuA4onsRa3
+# RSA_IPNS_IDv0=QmVujd5Vb7moysJj8itnGufN7MEtPRCNHkKpNuA4onsRa3
+# RSA_IPNS_IDv1=k2k4r8m7xvggw5pxxk3abrkwyer625hg01hfyggrai7lk1m63fuihi7w
+# RSA_IPNS_IDv1_DAGPB=k2jmtxu61bnhrtj301lw7zizknztocdbeqhxgv76l2q9t36fn9jbzipo
+
+# ED25519_KEY=12D3KooWLQzUv2FHWGVPXTXSZpdHs7oHbXub2G5WC8Tx4NQhyd2d
+# ED25519_IPNS_IDv0=12D3KooWLQzUv2FHWGVPXTXSZpdHs7oHbXub2G5WC8Tx4NQhyd2d
+# ED25519_IPNS_IDv1=k51qzi5uqu5dk3v4rmjber23h16xnr23bsggmqqil9z2gduiis5se8dht36dam
+# ED25519_IPNS_IDv1_DAGPB=k50rm9yjlt0jey4fqg6wafvqprktgbkpgkqdg27tpqje6iimzxewnhvtin9hhq
+# IPNS_ED25519_B58MH=12D3KooWLQzUv2FHWGVPXTXSZpdHs7oHbXub2G5WC8Tx4NQhyd2d
+# IPNS_ED25519_B36CID=k51qzi5uqu5dk3v4rmjber23h16xnr23bsggmqqil9z2gduiis5se8dht36dam
+```
diff --git a/fixtures/t0114/dnslink.yml b/fixtures/t0114/dnslink.yml
new file mode 100644
index 000000000..54c793b72
--- /dev/null
+++ b/fixtures/t0114/dnslink.yml
@@ -0,0 +1,10 @@
+# yaml-language-server: $schema=../fixture.schema.json
+dnslinks:
+ wikipedia:
+ domain: dnslink-subdomain-gw-test.example.org
+ # Wikipedia CID
+ path: /ipfs/bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze
+ test:
+ domain: dnslink-test.example.com
+ # CIDv1=$(echo "hello" | ipfs add --cid-version 1 -Q)
+ path: /ipfs/bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am
\ No newline at end of file
diff --git a/fixtures/t0114/fixtures.car b/fixtures/t0114/fixtures.car
new file mode 100644
index 000000000..bc7b913df
Binary files /dev/null and b/fixtures/t0114/fixtures.car differ
diff --git a/kubo-config.example.sh b/kubo-config.example.sh
index 14cf41969..48f17a82f 100755
--- a/kubo-config.example.sh
+++ b/kubo-config.example.sh
@@ -1,17 +1,22 @@
#! /usr/bin/env bash
-ipfs config --json Gateway.PublicGateways '{
- "example.com": {
- "UseSubdomains": true,
- "Paths": ["/ipfs", "/ipns", "/api"]
- },
- "localhost": {
- "UseSubdomains": true,
- "InlineDNSLink": true,
- "Paths": ["/ipfs", "/ipns", "/api"]
- }
+
+FIXTURES_PATH=${1:-$(pwd)}
+
+ipfs config --json Gateway.PublicGateways '{
+ "example.com": {
+ "UseSubdomains": true,
+ "InlineDNSLink": true,
+ "Paths": ["/ipfs", "/ipns", "/api"]
+ },
+ "localhost": {
+ "UseSubdomains": true,
+ "InlineDNSLink": true,
+ "Paths": ["/ipfs", "/ipns", "/api"]
+ }
}'
-export IPFS_NS_MAP=$(cat ./dnslinks.json | jq -r 'to_entries | map("\(.key).example.com:\(.value)") | join(",")')
+export IPFS_NS_MAP=$(cat "${FIXTURES_PATH}/dnslinks.json" | jq -r '.subdomains | to_entries | map("\(.key).example.com:\(.value)") | join(",")')
+export IPFS_NS_MAP="$(cat "${FIXTURES_PATH}/dnslinks.json" | jq -r '.domains | to_entries | map("\(.key):\(.value)") | join(",")'),${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/t0114_gateway_subdomains_test.go b/tests/t0114_gateway_subdomains_test.go
index c44e4d2d2..5713d6c7b 100644
--- a/tests/t0114_gateway_subdomains_test.go
+++ b/tests/t0114_gateway_subdomains_test.go
@@ -1,15 +1,18 @@
package tests
import (
- "fmt"
"net/url"
"testing"
"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/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"
)
func TestGatewaySubdomains(t *testing.T) {
@@ -25,16 +28,6 @@ func TestGatewaySubdomains(t *testing.T) {
tests := SugarTests{}
- // sugar: readable way to add more tests
- with := func(moreTests SugarTests) {
- tests = append(tests, moreTests...)
- }
-
- // sugar: nicer looking sprintf call
- URL := func(path string, args ...interface{}) string {
- return tmpl.Fmt(path, args...)
- }
-
// We're going to run the same test against multiple gateways (localhost, and a subdomain gateway)
gatewayURLs := []string{
SubdomainGatewayURL,
@@ -47,397 +40,706 @@ func TestGatewaySubdomains(t *testing.T) {
t.Fatal(err)
}
- with(testGatewayWithManyProtocols(t,
- "request for example.com/ipfs/{CIDv1} redirects to subdomain",
- `
- subdomains should not return payload directly,
- but redirect to URL with proper origin isolation
- `,
- URL("{{url}}/ipfs/{{cid}}/", gatewayURL, CIDv1),
- Expect().
- Status(301).
- Headers(
- Header("Location").
- Hint("request for example.com/ipfs/{CIDv1} returns Location HTTP header for subdomain redirect in browsers").
- Contains("{{scheme}}://{{cid}}.ipfs.{{host}}/", u.Scheme, CIDv1, u.Host),
- ),
- ))
-
- with(testGatewayWithManyProtocols(t,
- "request for example.com/ipfs/{DirCID} redirects to subdomain",
- `
- subdomains should not return payload directly,
- but redirect to URL with proper origin isolation
- `,
- URL("{{url}}/ipfs/{{cid}}/", gatewayURL, DirCID),
- Expect().
- Status(301).
- Headers(
- Header("Location").
- Hint("request for example.com/ipfs/{DirCID} returns Location HTTP header for subdomain redirect in browsers").
- Contains("{{scheme}}://{{cid}}.ipfs.{{host}}/", u.Scheme, DirCID, u.Host),
- ),
- ))
-
- with(testGatewayWithManyProtocols(t,
- "request for example.com/ipfs/{CIDv0} redirects to CIDv1 representation in subdomain",
- "",
- URL("{{url}}/ipfs/{{cid}}/", gatewayURL, CIDv0),
- 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}}/", u.Scheme, CIDv0to1, u.Host),
- ),
- ))
-
- // TODO: ipns
- // TODO: dns link test
-
- // ============================================================================
- // Test subdomain-based requests to a local gateway with default config
- // (origin per content root at http://*.example.com)
- // ============================================================================
-
- with(testGatewayWithManyProtocols(t,
- "request for {CID}.ipfs.example.com should return expected payload",
- "",
- URL("{{scheme}}://{{cid}}.ipfs.{{host}}", u.Scheme, CIDv1, u.Host),
- Expect().
- Status(200).
- Body(Contains(CIDVal)),
- ))
-
- with(testGatewayWithManyProtocols(t,
- "request for {CID}.ipfs.example.com/ipfs/{CID} should return HTTP 404",
- "ensure /ipfs/ namespace is not mounted on subdomain",
- URL("{{scheme}}://{{cid}}.ipfs.{{host}}/ipfs/{{cid}}", u.Scheme, CIDv1, u.Host),
- Expect().
- Status(404),
- ))
-
- with(testGatewayWithManyProtocols(t,
- "request for {CID}.ipfs.example.com/ipfs/file.txt should return data from a file in CID content root",
- "ensure requests to /ipfs/* are not blocked, if content root has such subdirectory",
- URL("{{scheme}}://{{cid}}.ipfs.{{host}}/ipfs/file.txt", u.Scheme, DirCID, u.Host),
- Expect().
- Status(200).
- Body(Contains("I am a txt file")),
- ))
-
- with(testGatewayWithManyProtocols(t,
- "valid file and subdirectory paths in directory listing at {cid}.ipfs.example.com",
- "{CID}.ipfs.example.com/sub/dir (Directory Listing)",
- URL("{{scheme}}://{{cid}}.ipfs.{{host}}/", u.Scheme, DirCID, u.Host),
- Expect().
- Status(200).
- Body(And(
- // TODO: implement html expectations
- Contains(`hello`),
- Contains(`ipfs`),
- )),
- ))
-
- with(testGatewayWithManyProtocols(t,
- "valid parent directory path in directory listing at {cid}.ipfs.example.com/sub/dir",
- "",
- URL("{{scheme}}://{{cid}}.ipfs.{{host}}/ipfs/ipns/", u.Scheme, DirCID, u.Host),
- Expect().
- Status(200).
- Body(And(
- // TODO: implement html expectations
- Contains(`..`),
- Contains(`bar`),
- )),
- ))
-
- with(testGatewayWithManyProtocols(t,
- "request for deep path resource at {cid}.ipfs.localhost/sub/dir/file",
- "",
- URL("{{scheme}}://{{cid}}.ipfs.{{host}}/ipfs/ipns/bar", u.Scheme, DirCID, u.Host),
- Expect().
- Status(200).
- Body(Contains("text-file-content")),
- ))
-
- with(testGatewayWithManyProtocols(t,
- "valid breadcrumb links in the header of directory listing at {cid}.ipfs.example.com/sub/dir",
- `
+ tests = append(tests, SugarTests{
+ {
+ Name: "request for example.com/ipfs/{CIDv1} redirects to subdomain",
+ Hint: `
+ subdomains should not return payload directly,
+ but redirect to URL with proper origin isolation
+ `,
+ Request: Request().URL("{{url}}/ipfs/{{cid}}/", gatewayURL, CIDv1),
+ Response: Expect().
+ Status(301).
+ Headers(
+ Header("Location").
+ Hint("request for example.com/ipfs/{CIDv1} returns Location HTTP header for subdomain redirect in browsers").
+ Contains("{{scheme}}://{{cid}}.ipfs.{{host}}/", u.Scheme, CIDv1, u.Host),
+ ),
+ },
+ {
+ Name: "request for example.com/ipfs/{DirCID} redirects to subdomain",
+ Hint: `
+ subdomains should not return payload directly,
+ but redirect to URL with proper origin isolation
+ `,
+ Request: Request().URL("{{url}}/ipfs/{{cid}}/", gatewayURL, DirCID),
+ Response: Expect().
+ Status(301).
+ Headers(
+ Header("Location").
+ Hint("request for example.com/ipfs/{DirCID} returns Location HTTP header for subdomain redirect in browsers").
+ Contains("{{scheme}}://{{cid}}.ipfs.{{host}}/", u.Scheme, DirCID, u.Host),
+ ),
+ },
+ {
+ Name: "request for example.com/ipfs/{CIDv0} redirects to CIDv1 representation in subdomain",
+ Request: Request().URL("{{url}}/ipfs/{{cid}}/", gatewayURL, 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}}/", u.Scheme, CIDv0to1, u.Host),
+ ),
+ },
+ // ============================================================================
+ // Test subdomain-based requests to a local gateway with default config
+ // (origin per content root at http://*.example.com)
+ // ============================================================================
+ {
+ Name: "request for {CID}.ipfs.example.com should return expected payload",
+ Request: Request().URL("{{scheme}}://{{cid}}.ipfs.{{host}}", u.Scheme, CIDv1, u.Host),
+ 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),
+ 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),
+ 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),
+ Response: Expect().
+ Status(200).
+ Body(And(
+ // TODO: implement html expectations
+ 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),
+ Response: Expect().
+ Status(200).
+ Body(And(
+ // TODO: implement html expectations
+ Contains(`..`),
+ Contains(`bar`),
+ )),
+ },
+ {
+ Name: "request for deep path resource at {cid}.ipfs.localhost/sub/dir/file",
+ Request: Request().URL("{{scheme}}://{{cid}}.ipfs.{{host}}/ipfs/ipns/bar", u.Scheme, DirCID, u.Host),
+ Response: Expect().
+ Status(200).
+ Body(Contains("text-file-content")),
+ },
+ {
+ Name: "valid breadcrumb links in the header of directory listing at {cid}.ipfs.example.com/sub/dir",
+ Hint: `
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
`,
- URL("{{scheme}}://{{cid}}.ipfs.{{host}}/ipfs/ipns/", u.Scheme, DirCID, u.Host),
- Expect().
- Status(200).
- Body(
- And(
- Contains("Index of"),
- Contains(`/ipfs/{{cid}}/ipfs/ipns`,
- u.Host, DirCID),
+ Request: Request().URL("{{scheme}}://{{cid}}.ipfs.{{host}}/ipfs/ipns/", u.Scheme, DirCID, u.Host),
+ Response: Expect().
+ Status(200).
+ Body(
+ And(
+ Contains("Index of"),
+ Contains(`/ipfs/{{cid}}/ipfs/ipns`,
+ u.Host, DirCID),
+ ),
+ ),
+ },
+ // ## ============================================================================
+ // ## Test subdomain-based requests with a custom hostname config
+ // ## (origin per content root at http://*.example.com)
+ // ## ============================================================================
+
+ // # example.com/ip(f|n)s/*
+ // # =============================================================================
+
+ // # path requests to the root hostname should redirect
+ // # to a subdomain URL with proper origin isolation
+
+ {
+ 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),
+ 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),
+ Response: Expect().
+ Status(301).
+ Headers(
+ Header("Location").Equals("{{scheme}}://{{cid}}.ipfs.{{host}}/", u.Scheme, CIDv0to1, u.Host),
+ ),
+ },
+
+ {
+ Name: "request for http://example.com/ipfs/{CID} with X-Forwarded-Proto: https produces redirect to HTTPS URL",
+ Hint: "Support X-Forwarded-Proto",
+ Request: Request().URL("{{scheme}}://{{host}}/ipfs/{{cid}}/", u.Scheme, u.Host, CIDv1).
+ Header("X-Forwarded-Proto", "https"),
+ Response: Expect().
+ Status(301).
+ Headers(
+ Header("Location").Equals("https://{{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,
+ ),
+ Response: Expect().
+ Status(301).
+ Headers(
+ Header("Location").Equals("/ipfs/{{cid}}/wiki/Diego_Maradona.html", CIDWikipedia),
+ ),
+ },
+ {
+ Name: "request for a too long CID at localhost/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),
+ 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.localhost 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),
+ Response: Expect().
+ Status(400).
+ Body(Contains("CID incompatible with DNS label length limit of 63")),
+ },
+ // ## ============================================================================
+ // ## Test support for X-Forwarded-Host
+ // ## ============================================================================
+ {
+ Name: "request for http://fake.domain.com/ipfs/{CID} doesn't match the example.com gateway",
+ Request: Request().URL("{{scheme}}://{{domain}}/ipfs/{{cid}}", u.Scheme, "fake.domain.com", 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",
+ Request: Request().URL("{{scheme}}://{{domain}}/ipfs/{{cid}}", u.Scheme, "fake.domain.com", CIDv1).
+ Header("X-Forwarded-Host", u.Host),
+ Response: Expect().
+ Status(301).
+ Headers(
+ Header("Location").Equals("{{scheme}}://{{cid}}.ipfs.{{host}}/", u.Scheme, CIDv1, u.Host),
),
- ),
- ))
-
- // TODO: # *.ipns.localhost
- // TODO: # .ipns.localhost
- // TODO: # .ipns.localhost
-
- // ## ============================================================================
- // ## Test DNSLink inlining on HTTP gateways
- // ## ============================================================================
-
- // TODO
-
- // ## ============================================================================
- // ## Test subdomain-based requests with a custom hostname config
- // ## (origin per content root at http://*.example.com)
- // ## ============================================================================
-
- // # example.com/ip(f|n)s/*
- // # =============================================================================
-
- // # path requests to the root hostname should redirect
- // # to a subdomain URL with proper origin isolation
-
- with(testGatewayWithManyProtocols(t,
- "request for example.com/ipfs/{CIDv1} produces redirect to {CIDv1}.ipfs.example.com",
- "path requests to the root hostname should redirect to a subdomain URL with proper origin isolation",
- URL("{{scheme}}://{{host}}/ipfs/{{cid}}/", u.Scheme, u.Host, CIDv1),
- Expect().
- Headers(
- Header("Location").Equals("{{scheme}}://{{cid}}.ipfs.{{host}}/", u.Scheme, CIDv1, u.Host),
- ),
- ))
-
- with(testGatewayWithManyProtocols(t,
- "request for example.com/ipfs/{InvalidCID} produces useful error before redirect",
- "error message should include original CID (and it should be case-sensitive, as we can't assume everyone uses base32)",
- URL("{{scheme}}://{{host}}/ipfs/QmInvalidCID", u.Scheme, u.Host),
- Expect().
- Body(Contains(`invalid path "/ipfs/QmInvalidCID"`)),
- ))
-
- with(testGatewayWithManyProtocols(t,
- "request for example.com/ipfs/{CIDv0} produces redirect to {CIDv1}.ipfs.example.com",
- "",
- URL("{{scheme}}://{{host}}/ipfs/{{cid}}/", u.Scheme, u.Host, CIDv0),
- Expect().
- Status(301).
- Headers(
- Header("Location").Equals("{{scheme}}://{{cid}}.ipfs.{{host}}/", u.Scheme, CIDv0to1, u.Host),
- ),
- ))
-
- with(testGatewayWithManyProtocols(t,
- "request for http://example.com/ipfs/{CID} with X-Forwarded-Proto: https produces redirect to HTTPS URL",
- "Support X-Forwarded-Proto",
- Request().
- URL("{{scheme}}://{{host}}/ipfs/{{cid}}/", u.Scheme, u.Host, CIDv1).
- Header("X-Forwarded-Proto", "https"),
- Expect().
- Status(301).
- Headers(
- Header("Location").Equals("https://{{cid}}.ipfs.{{host}}/", CIDv1, u.Host),
- ),
- ))
-
- with(testGatewayWithManyProtocols(t,
- "request for example.com/ipfs/?uri=ipfs%3A%2F%2F.. produces redirect to /ipfs/.. content path",
- "Support ipfs:// in https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler",
- Request().
- URL("{{scheme}}://{{host}}/ipfs/", u.Scheme, u.Host).
- Query(
- "uri", "ipfs://{{host}}/wiki/Diego_Maradona.html", CIDWikipedia,
- ),
- Expect().
- Status(301).
- Headers(
- Header("Location").Equals("/ipfs/{{cid}}/wiki/Diego_Maradona.html", CIDWikipedia),
- ),
- ))
-
- // # example.com/ipns/
- // TODO
-
- // # example.com/ipns/
- // TODO
-
- // # DNSLink on Public gateway with a single-level wildcard TLS cert
- // # "Option C" from https://github.com/ipfs/in-web-browsers/issues/169
- // TODO
-
- // # Support ipns:// in https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler
- // TODO
-
- // # *.ipns.example.com
- // # ============================================================================
-
- // # .ipns.example.com
-
- // # API on subdomain gateway example.com
- // # ============================================================================
-
- // # DNSLink: .ipns.example.com
- // # (not really useful outside of localhost, as setting TLS for more than one
- // # level of wildcard is a pain, but we support it if someone really wants it)
- // # ============================================================================
- // TODO
-
- // # DNSLink on Public gateway with a single-level wildcard TLS cert
- // # "Option C" from https://github.com/ipfs/in-web-browsers/issues/169
-
- // ## Test subdomain handling of CIDs that do not fit in a single DNS Label (>63chars)
- // ## https://github.com/ipfs/go-ipfs/issues/7318
- // ## ============================================================================
- // TODO
-
- with(testGatewayWithManyProtocols(t,
- "request for a too long CID at localhost/ipfs/{CIDv1} returns human readable error",
- "router should not redirect to hostnames that could fail due to DNS limits",
- URL("{{url}}/ipfs/{{cid}}", gatewayURL, CIDv1_TOO_LONG),
- Expect().
- Status(400).
- Body(Contains("CID incompatible with DNS label length limit of 63")),
- ))
-
- with(testGatewayWithManyProtocols(t,
- "request for a too long CID at {CIDv1}.ipfs.localhost returns expected payload",
- "direct request should also fail (provides the same UX as router and avoids confusion)",
- URL("{{scheme}}://{{cid}}.ipfs.{{host}}/", u.Scheme, CIDv1_TOO_LONG, u.Host),
- Expect().
- Status(400).
- Body(Contains("CID incompatible with DNS label length limit of 63")),
- ))
-
- // # public subdomain gateway: *.example.com
- // TODO: IPNS
-
- // # Disable selected Paths for the subdomain gateway hostname
- // # =============================================================================
-
- // # disable /ipns for the hostname by not whitelisting it
-
- // # refuse requests to Paths that were not explicitly whitelisted for the hostname
-
- // MANY TODOs here
-
- // ## ============================================================================
- // ## Test support for X-Forwarded-Host
- // ## ============================================================================
-
- with(testGatewayWithManyProtocols(t,
- "request for http://fake.domain.com/ipfs/{CID} doesn't match the example.com gateway",
- "",
- URL("{{scheme}}://{{domain}}/ipfs/{{cid}}", u.Scheme, "fake.domain.com", CIDv1),
- Expect().
- Status(200),
- ))
-
- with(testGatewayWithManyProtocols(t,
- "request for http://fake.domain.com/ipfs/{CID} with X-Forwarded-Host: example.com match the example.com gateway",
- "",
- Request().
- URL("{{scheme}}://{{domain}}/ipfs/{{cid}}", u.Scheme, "fake.domain.com", CIDv1).
- Header("X-Forwarded-Host", u.Host),
- Expect().
- Status(301).
- Headers(
- Header("Location").Equals("{{scheme}}://{{cid}}.ipfs.{{host}}/", u.Scheme, CIDv1, u.Host),
- ),
- ))
-
- with(testGatewayWithManyProtocols(t,
- "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",
- "",
- Request().
- URL("{{scheme}}://{{domain}}/ipfs/{{cid}}", u.Scheme, "fake.domain.com", CIDv1).
- Header("X-Forwarded-Host", u.Host).
- Header("X-Forwarded-Proto", "https"),
- Expect().
- Status(301).
- Headers(
- Header("Location").Equals("https://{{cid}}.ipfs.{{host}}/", CIDv1, u.Host),
- ),
- ))
+ },
+ {
+ 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",
+ Request: Request().URL("{{scheme}}://{{domain}}/ipfs/{{cid}}", u.Scheme, "fake.domain.com", CIDv1).
+ Header("X-Forwarded-Host", u.Host).
+ Header("X-Forwarded-Proto", "https"),
+ Response: Expect().
+ Status(301).
+ Headers(
+ Header("Location").Equals("https://{{cid}}.ipfs.{{host}}/", CIDv1, u.Host),
+ ),
+ },
+ }...)
}
- if specs.SubdomainGateway.IsEnabled() {
- Run(t, tests)
- } else {
- t.Skip("subdomain gateway disabled")
- }
+ RunIfSpecsAreEnabled(t, helpers.UnwrapSubdomainTests(t, tests), specs.SubdomainGateway)
}
-func testGatewayWithManyProtocols(t *testing.T, label string, hint string, reqURL interface{}, expected ExpectBuilder) SugarTests {
- t.Helper()
+func TestGatewaySubdomainAndIPNS(t *testing.T) {
+ tests := SugarTests{}
+
+ rsaFixture := ipns.MustOpenIPNSRecordWithKey("t0114/QmVujd5Vb7moysJj8itnGufN7MEtPRCNHkKpNuA4onsRa3.ipns-record")
+ ed25519Fixture := ipns.MustOpenIPNSRecordWithKey("t0114/12D3KooWLQzUv2FHWGVPXTXSZpdHs7oHbXub2G5WC8Tx4NQhyd2d.ipns-record")
- baseURL := ""
- baseReq := Request()
+ car := car.MustOpenUnixfsCar("t0114/fixtures.car")
+ helloCID := "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am"
+ payload := string(car.MustGetRawData(helloCID))
- switch req := reqURL.(type) {
- case string:
- baseURL = reqURL.(string)
- case RequestBuilder:
- baseReq = req
- baseURL = req.GetURL()
- default:
- t.Fatalf("invalid type for reqURL: %T", reqURL)
+ // We're going to run the same test against multiple gateways (localhost, and a subdomain gateway)
+ gatewayURLs := []string{
+ SubdomainGatewayURL,
+ SubdomainLocalhostGatewayURL,
}
- u, err := url.Parse(baseURL)
- if err != nil {
- t.Fatal(err)
+ ipnsRecords := []*ipns.IpnsRecord{
+ rsaFixture,
+ ed25519Fixture,
}
- // Because you might be testing an IPFS node in CI, or on your local machine, the test are designed
- // to test the subdomain behavior (querying http://{CID}.my-subdomain-gateway.io/) even if the node is
- // actually living on http://127.0.0.1:8080 or somewhere else.
- //
- // The test knows two addresses:
- // - GatewayURL: the URL we connect to, it might be "dweb.link", "127.0.0.1:8080", etc.
- // - SubdomainGatewayURL: the URL we test for subdomain requests, it might be "dweb.link", "localhost", "example.com", etc.
-
- // host is the hostname of the gateway we are testing, it might be `localhost` or `example.com`
- host := u.Host
-
- // raw url is the url but we replace the host with our local url, it might be `http://127.0.0.1/ipfs/something`
- u.Host = GatewayHost
- rawURL := u.String()
-
- return SugarTests{
- {
- Name: fmt.Sprintf("%s (direct HTTP)", label),
- Hint: fmt.Sprintf("%s\n%s", hint, "direct HTTP request (hostname in URL, raw IP in Host header)"),
- Request: baseReq.
- URL(rawURL).
- Headers(
- Header("Host", host),
- ),
- Response: expected,
- },
- {
- Name: fmt.Sprintf("%s (HTTP proxy)", label),
- Hint: fmt.Sprintf("%s\n%s", hint, "HTTP proxy (hostname is passed via URL)"),
- Request: baseReq.
- URL(baseURL).
- Proxy(GatewayURL),
- Response: expected,
- },
- {
- Name: fmt.Sprintf("%s (HTTP proxy tunneling via CONNECT)", label),
- Hint: fmt.Sprintf("%s\n%s", 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: baseReq.
- URL(baseURL).
- Proxy(GatewayURL).
- WithProxyTunnel().
- Headers(
- Header("Host", host),
- ),
- Response: expected,
- },
+
+ for _, gatewayURL := range gatewayURLs {
+ u, err := url.Parse(gatewayURL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, record := range ipnsRecords {
+ tests = append(tests, SugarTests{
+ // # /ipns/
+ // test_localhost_gateway_response_should_contain \
+ // "request for localhost/ipns/{CIDv0} redirects to CIDv1 with libp2p-key multicodec in subdomain" \
+ // "http://localhost:$GWAY_PORT/ipns/$RSA_IPNS_IDv0" \
+ // "Location: http://${RSA_IPNS_IDv1}.ipns.localhost:$GWAY_PORT/"
+ // test_localhost_gateway_response_should_contain \
+ // "request for localhost/ipns/{CIDv0} redirects to CIDv1 with libp2p-key multicodec in subdomain" \
+ // "http://localhost:$GWAY_PORT/ipns/$ED25519_IPNS_IDv0" \
+ // "Location: http://${ED25519_IPNS_IDv1}.ipns.localhost:$GWAY_PORT/"
+ {
+ Name: "request for /ipns/{CIDv0} redirects to CIDv1 with libp2p-key multicodec in subdomain",
+ Request: Request().
+ URL("{{url}}/ipns/{{cid}}", gatewayURL, record.IdV0()),
+ Response: Expect().
+ Status(301).
+ Headers(
+ Header("Location").
+ Equals("{{scheme}}://{{cid}}.ipns.{{host}}/", u.Scheme, record.IdV1(), u.Host),
+ ),
+ },
+ // # *.ipns.localhost
+ // # .ipns.localhost
+ // test_localhost_gateway_response_should_contain \
+ // "request for {CIDv1-libp2p-key}.ipns.localhost returns expected payload" \
+ // "http://${RSA_IPNS_IDv1}.ipns.localhost:$GWAY_PORT" \
+ // "$CID_VAL"
+ // test_localhost_gateway_response_should_contain \
+ // "request for {CIDv1-libp2p-key}.ipns.localhost returns expected payload" \
+ // "http://${ED25519_IPNS_IDv1}.ipns.localhost:$GWAY_PORT" \
+ // "$CID_VAL"
+ {
+ Name: "request for {CIDv1-libp2p-key}.ipns.{gateway} returns expected payload",
+ Request: Request().
+ URL("{{scheme}}://{{cid}}.ipns.{{host}}/", u.Scheme, record.IdV1(), u.Host),
+ Response: Expect().
+ Status(200).
+ BodyWithHint("Request for {{cid}}.ipns.{{host}} returns expected payload", payload),
+ },
+ // test_localhost_gateway_response_should_contain \
+ // "localhost request for {CIDv1-dag-pb}.ipns.localhost redirects to CID with libp2p-key multicodec" \
+ // "http://${RSA_IPNS_IDv1_DAGPB}.ipns.localhost:$GWAY_PORT" \
+ // "Location: http://${RSA_IPNS_IDv1}.ipns.localhost:$GWAY_PORT/"
+ // test_localhost_gateway_response_should_contain \
+ // "localhost request for {CIDv1-dag-pb}.ipns.localhost redirects to CID with libp2p-key multicodec" \
+ // "http://${ED25519_IPNS_IDv1_DAGPB}.ipns.localhost:$GWAY_PORT" \
+ // "Location: http://${ED25519_IPNS_IDv1}.ipns.localhost:$GWAY_PORT/"
+ {
+ 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),
+ Response: Expect().
+ Status(301).
+ Headers(
+ Header("Location").
+ Equals("{{scheme}}://{{cid}}.ipns.{{host}}/", u.Scheme, record.IdV1(), u.Host),
+ ),
+ },
+ // # example.com/ipns/
+ // test_hostname_gateway_response_should_contain \
+ // "request for example.com/ipns/{CIDv0} redirects to CIDv1 with libp2p-key multicodec in subdomain" \
+ // "example.com" \
+ // "http://127.0.0.1:$GWAY_PORT/ipns/$RSA_IPNS_IDv0" \
+ // "Location: http://${RSA_IPNS_IDv1}.ipns.example.com/"
+ // test_hostname_gateway_response_should_contain \
+ // "request for example.com/ipns/{CIDv0} redirects to CIDv1 with libp2p-key multicodec in subdomain" \
+ // "example.com" \
+ // "http://127.0.0.1:$GWAY_PORT/ipns/$ED25519_IPNS_IDv0" \
+ // "Location: http://${ED25519_IPNS_IDv1}.ipns.example.com/"
+ // NOTE: Done above, thanks to the loop
+ //
+ // # *.ipns.example.com
+ // # ============================================================================
+
+ // # .ipns.example.com
+
+ // test_hostname_gateway_response_should_contain \
+ // "request for {CIDv1-libp2p-key}.ipns.example.com returns expected payload" \
+ // "${RSA_IPNS_IDv1}.ipns.example.com" \
+ // "http://127.0.0.1:$GWAY_PORT" \
+ // "$CID_VAL"
+
+ // test_hostname_gateway_response_should_contain \
+ // "request for {CIDv1-libp2p-key}.ipns.example.com returns expected payload" \
+ // "${ED25519_IPNS_IDv1}.ipns.example.com" \
+ // "http://127.0.0.1:$GWAY_PORT" \
+ // "$CID_VAL"
+
+ // test_hostname_gateway_response_should_contain \
+ // "hostname request for {CIDv1-dag-pb}.ipns.localhost redirects to CID with libp2p-key multicodec" \
+ // "${RSA_IPNS_IDv1_DAGPB}.ipns.example.com" \
+ // "http://127.0.0.1:$GWAY_PORT" \
+ // "Location: http://${RSA_IPNS_IDv1}.ipns.example.com/"
+
+ // test_hostname_gateway_response_should_contain \
+ // "hostname request for {CIDv1-dag-pb}.ipns.localhost redirects to CID with libp2p-key multicodec" \
+ // "${ED25519_IPNS_IDv1_DAGPB}.ipns.example.com" \
+ // "http://127.0.0.1:$GWAY_PORT" \
+ // "Location: http://${ED25519_IPNS_IDv1}.ipns.example.com/"
+ // # disable /ipns for the hostname by not whitelisting it
+ // ipfs config --json Gateway.PublicGateways '{
+ // "example.com": {
+ // "UseSubdomains": true,
+ // "Paths": ["/ipfs"]
+ // }
+ // }' || exit 1
+ // # restart daemon to apply config changes
+ // test_kill_ipfs_daemon
+ // test_launch_ipfs_daemon_without_network
+
+ // TODO: what to do with these?
+ // # refuse requests to Paths that were not explicitly whitelisted for the hostname
+ // test_hostname_gateway_response_should_contain \
+ // "request for *.ipns.example.com returns HTTP 404 Not Found when /ipns is not on Paths whitelist" \
+ // "${RSA_IPNS_IDv1}.ipns.example.com" \
+ // "http://127.0.0.1:$GWAY_PORT" \
+ // "404 Not Found"
+
+ // test_hostname_gateway_response_should_contain \
+ // "request for *.ipns.example.com returns HTTP 404 Not Found when /ipns is not on Paths whitelist" \
+ // "${ED25519_IPNS_IDv1}.ipns.example.com" \
+ // "http://127.0.0.1:$GWAY_PORT" \
+ // "404 Not Found"
+
+ // # refuse requests to Paths that were not explicitly whitelisted for the hostname
+ // test_hostname_gateway_response_should_contain \
+ // "request for example.com/ipns/ returns HTTP 404 Not Found when /ipns is not on Paths whitelist" \
+ // "example.com" \
+ // "http://127.0.0.1:$GWAY_PORT/ipns/$RSA_IPNS_IDv1" \
+ // "404 Not Found"
+
+ // test_hostname_gateway_response_should_contain \
+ // "request for example.com/ipns/ returns HTTP 404 Not Found when /ipns is not on Paths whitelist" \
+ // "example.com" \
+ // "http://127.0.0.1:$GWAY_PORT/ipns/$ED25519_IPNS_IDv1" \
+ // "404 Not Found"
+ }...)
+ }
+
+ tests = append(tests, SugarTests{
+ // ## Test subdomain handling of CIDs that do not fit in a single DNS Label (>63chars)
+ // ## https://github.com/ipfs/go-ipfs/issues/7318
+ // ## ============================================================================
+ // # local: *.localhost
+ // test_localhost_gateway_response_should_contain \
+ // "request for a ED25519 libp2p-key at localhost/ipns/{b58mh} returns Location HTTP header for DNS-safe subdomain redirect in browsers" \
+ // "http://localhost:$GWAY_PORT/ipns/$IPNS_ED25519_B58MH" \
+ // "Location: http://${IPNS_ED25519_B36CID}.ipns.localhost:$GWAY_PORT/"
+ // # public subdomain gateway: *.example.com
+ // test_hostname_gateway_response_should_contain \
+ // "request for a ED25519 libp2p-key at example.com/ipns/{b58mh} returns Location HTTP header for DNS-safe subdomain redirect in browsers" \
+ // "example.com" \
+ // "http://127.0.0.1:$GWAY_PORT/ipns/$IPNS_ED25519_B58MH" \
+ // "Location: http://${IPNS_ED25519_B36CID}.ipns.example.com"
+ {
+ 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()),
+ Response: Expect().
+ Headers(
+ Header("Location").
+ Equals("{{scheme}}://{{cid}}.ipns.{{host}}/", u.Scheme, ed25519Fixture.ToCID(multicodec.Libp2pKey, multibase.Base36), u.Host),
+ ),
+ },
+ }...)
+
+ }
+
+ RunIfSpecsAreEnabled(t, helpers.UnwrapSubdomainTests(t, tests), specs.SubdomainGateway, specs.IPNSResolver)
+}
+
+func TestGatewaySubdomainAndDnsLink(t *testing.T) {
+ tests := SugarTests{}
+
+ // We're going to run the same test against multiple gateways (localhost, and a subdomain gateway)
+ gatewayURLs := []string{
+ SubdomainGatewayURL,
+ SubdomainLocalhostGatewayURL,
+ }
+
+ dnsLinks := dnslink.MustOpenDNSLink("t0114/dnslink.yml")
+ wikipedia := dnsLinks.MustGet("wikipedia")
+ dnsLinkTest := dnsLinks.MustGet("test")
+
+ for _, gatewayURL := range gatewayURLs {
+ u, err := url.Parse(gatewayURL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ tests = append(tests, SugarTests{
+ // # /ipns/
+ // test_localhost_gateway_response_should_contain \
+ // "request for localhost/ipns/{fqdn} redirects to DNSLink in subdomain" \
+ // "http://localhost:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki" \
+ // "Location: http://en.wikipedia-on-ipfs.org.ipns.localhost:$GWAY_PORT/wiki"
+ {
+ Name: "request for /ipns/{fqdn} redirects to DNSLink in subdomain",
+ Request: Request().
+ URL("{{url}}/ipns/{{fqdn}}/wiki/", gatewayURL, wikipedia),
+ Response: Expect().
+ Headers(
+ Header("Location").
+ Equals("{{scheme}}://{{fqdn}}.ipns.{{host}}/wiki/", u.Scheme, dnslink.InlineDNS(wikipedia), u.Host),
+ ),
+ },
+ // # .ipns.localhost
+ // # DNSLink test requires a daemon in online mode with precached /ipns/ mapping
+ // test_kill_ipfs_daemon
+ // DNSLINK_FQDN="dnslink-test.example.com"
+ // export IPFS_NS_MAP="$DNSLINK_FQDN:/ipfs/$CIDv1"
+ // test_launch_ipfs_daemon
+ // test_localhost_gateway_response_should_contain \
+ // "request for {dnslink}.ipns.localhost returns expected payload" \
+ // "http://$DNSLINK_FQDN.ipns.localhost:$GWAY_PORT" \
+ // "$CID_VAL"
+ {
+ Name: "request for {dnslink}.ipns.{gateway} returns expected payload",
+ Request: Request().
+ URL("{{scheme}}://{{fqdn}}.ipns.{{host}}", u.Scheme, dnsLinkTest, u.Host),
+ Response: Expect().
+ Body("hello\n"),
+ },
+ // ## ============================================================================
+ // ## Test DNSLink inlining on HTTP gateways
+ // ## ============================================================================
+ // # set explicit subdomain gateway config for the hostname
+ // ipfs config --json Gateway.PublicGateways '{
+ // "localhost": {
+ // "UseSubdomains": true,
+ // "InlineDNSLink": true,
+ // "Paths": ["/ipfs", "/ipns", "/api"]
+ // },
+ // "example.com": {
+ // "UseSubdomains": true,
+ // "InlineDNSLink": true,
+ // "Paths": ["/ipfs", "/ipns", "/api"]
+ // }
+ // }' || exit 1
+ // # restart daemon to apply config changes
+ // test_kill_ipfs_daemon
+ // test_launch_ipfs_daemon_without_network
+
+ // test_localhost_gateway_response_should_contain \
+ // "request for localhost/ipns/{fqdn} redirects to DNSLink in subdomain with DNS inlining" \
+ // "http://localhost:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki" \
+ // "Location: http://en-wikipedia--on--ipfs-org.ipns.localhost:$GWAY_PORT/wiki"
+
+ // test_hostname_gateway_response_should_contain \
+ // "request for example.com/ipns/{fqdn} redirects to DNSLink in subdomain with DNS inlining" \
+ // "example.com" \
+ // "http://127.0.0.1:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki" \
+ // "Location: http://en-wikipedia--on--ipfs-org.ipns.example.com/wiki"
+
+ // # example.com/ipns/
+
+ // test_hostname_gateway_response_should_contain \
+ // "request for example.com/ipns/{fqdn} redirects to DNSLink in subdomain" \
+ // "example.com" \
+ // "http://127.0.0.1:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki" \
+ // "Location: http://en.wikipedia-on-ipfs.org.ipns.example.com/wiki"
+
+ // # DNSLink on Public gateway with a single-level wildcard TLS cert
+ // # "Option C" from https://github.com/ipfs/in-web-browsers/issues/169
+ // test_expect_success \
+ // "request for example.com/ipns/{fqdn} with X-Forwarded-Proto redirects to TLS-safe label in subdomain" "
+ // curl -H \"Host: example.com\" -H \"X-Forwarded-Proto: https\" -sD - \"http://127.0.0.1:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki\" > response &&
+ // test_should_contain \"Location: https://en-wikipedia--on--ipfs-org.ipns.example.com/wiki\" response
+ // "
+ {
+ Name: "request for example.com/ipns/{fqdn} with X-Forwarded-Proto redirects to TLS-safe label in subdomain",
+ Hint: `
+ DNSLink on Public gateway with a single-level wildcard TLS cert
+ "Option C" from https://github.com/ipfs/in-web-browsers/issues/169
+ `,
+ Request: Request().
+ Header("X-Forwarded-Proto", "https").
+ URL("{{url}}/ipns/{{wikipedia}}/wiki/", gatewayURL, wikipedia),
+ Response: Expect().
+ Headers(
+ Header("Location").
+ Equals("https://{{inlined}}.ipns.{{host}}/wiki/", dnslink.InlineDNS(wikipedia), u.Host),
+ ),
+ },
+ // # Support ipns:// in https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler
+ // test_hostname_gateway_response_should_contain \
+ // "request for example.com/ipns/?uri=ipns%3A%2F%2F.. produces redirect to /ipns/.. content path" \
+ // "example.com" \
+ // "http://127.0.0.1:$GWAY_PORT/ipns/?uri=ipns%3A%2F%2Fen.wikipedia-on-ipfs.org" \
+ // "Location: /ipns/en.wikipedia-on-ipfs.org"
+ {
+ 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().
+ URL(`{{url}}/ipns/?uri=ipns%3A%2F%2F{{dnslink}}`, gatewayURL, wikipedia),
+ Response: Expect().
+ Headers(
+ Header("Location").Equals("/ipns/{{wikipedia}}", wikipedia),
+ ),
+ },
+ // # DNSLink: .ipns.example.com
+ // # (not really useful outside of localhost, as setting TLS for more than one
+ // # level of wildcard is a pain, but we support it if someone really wants it)
+ // # ============================================================================
+
+ // # DNSLink test requires a daemon in online mode with precached /ipns/ mapping
+ // test_kill_ipfs_daemon
+ // DNSLINK_FQDN="dnslink-subdomain-gw-test.example.org"
+ // export IPFS_NS_MAP="$DNSLINK_FQDN:/ipfs/$CIDv1"
+ // test_launch_ipfs_daemon
+
+ // test_hostname_gateway_response_should_contain \
+ // "request for {dnslink}.ipns.example.com returns expected payload" \
+ // "$DNSLINK_FQDN.ipns.example.com" \
+ // "http://127.0.0.1:$GWAY_PORT" \
+ // "$CID_VAL"
+ // Note: this test was merged with the test for wikipedia in the end.
+
+ // # DNSLink on Public gateway with a single-level wildcard TLS cert
+ // # "Option C" from https://github.com/ipfs/in-web-browsers/issues/169
+ // test_expect_success \
+ // "request for {single-label-dnslink}.ipns.example.com with X-Forwarded-Proto returns expected payload" "
+ // curl -H \"Host: dnslink--subdomain--gw--test-example-org.ipns.example.com\" -H \"X-Forwarded-Proto: https\" -sD - \"http://127.0.0.1:$GWAY_PORT\" > response &&
+ // test_should_contain \"$CID_VAL\" response
+ // "
+ // Note: this test was merged with the test for wikipedia in the end.
+
+ // ## ============================================================================
+ // ## Test DNSLink requests with a custom PublicGateway (hostname config)
+ // ## (DNSLink site at http://dnslink-test.example.com)
+ // ## ============================================================================
+ // # disable wildcard DNSLink gateway
+ // # and enable it on specific NSLink hostname
+ // ipfs config --json Gateway.NoDNSLink true && \
+ // ipfs config --json Gateway.PublicGateways '{
+ // "dnslink-enabled-on-fqdn.example.org": {
+ // "NoDNSLink": false,
+ // "UseSubdomains": false,
+ // "Paths": ["/ipfs"]
+ // },
+ // "only-dnslink-enabled-on-fqdn.example.org": {
+ // "NoDNSLink": false,
+ // "UseSubdomains": false,
+ // "Paths": []
+ // },
+ // "dnslink-disabled-on-fqdn.example.com": {
+ // "NoDNSLink": true,
+ // "UseSubdomains": false,
+ // "Paths": []
+ // }
+ // }' || exit 1
+
+ // # DNSLink test requires a daemon in online mode with precached /ipns/ mapping
+ // DNSLINK_FQDN="dnslink-enabled-on-fqdn.example.org"
+ // ONLY_DNSLINK_FQDN="only-dnslink-enabled-on-fqdn.example.org"
+ // NO_DNSLINK_FQDN="dnslink-disabled-on-fqdn.example.com"
+ // export IPFS_NS_MAP="$DNSLINK_FQDN:/ipfs/$CIDv1,$ONLY_DNSLINK_FQDN:/ipfs/$DIR_CID"
+
+ // # DNSLink enabled
+
+ // test_hostname_gateway_response_should_contain \
+ // "request for http://{dnslink-fqdn}/ PublicGateway returns expected payload" \
+ // "$DNSLINK_FQDN" \
+ // "http://127.0.0.1:$GWAY_PORT/" \
+ // "$CID_VAL"
+
+ // test_hostname_gateway_response_should_contain \
+ // "request for {dnslink-fqdn}/ipfs/{cid} returns expected payload when /ipfs is on Paths whitelist" \
+ // "$DNSLINK_FQDN" \
+ // "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \
+ // "$CID_VAL"
+
+ // # Test for a fun edge case: DNSLink-only gateway without /ipfs/ namespace
+ // # mounted, and with subdirectory named "ipfs" ¯\_(ツ)_/¯
+ // test_hostname_gateway_response_should_contain \
+ // "request for {dnslink-fqdn}/ipfs/file.txt returns data from content root when /ipfs in not on Paths whitelist" \
+ // "$ONLY_DNSLINK_FQDN" \
+ // "http://127.0.0.1:$GWAY_PORT/ipfs/file.txt" \
+ // "I am a txt file"
+
+ // test_hostname_gateway_response_should_contain \
+ // "request for {dnslink-fqdn}/ipns/{peerid} returns 404 when path is not whitelisted" \
+ // "$DNSLINK_FQDN" \
+ // "http://127.0.0.1:$GWAY_PORT/ipns/$RSA_IPNS_IDv0" \
+ // "404 Not Found"
+
+ // test_hostname_gateway_response_should_contain \
+ // "request for {dnslink-fqdn}/ipns/{peerid} returns 404 when path is not whitelisted" \
+ // "$DNSLINK_FQDN" \
+ // "http://127.0.0.1:$GWAY_PORT/ipns/$ED25519_IPNS_IDv0" \
+ // "404 Not Found"
+
+ // # DNSLink disabled
+
+ // test_hostname_gateway_response_should_contain \
+ // "request for http://{dnslink-fqdn}/ returns 404 when NoDNSLink=true" \
+ // "$NO_DNSLINK_FQDN" \
+ // "http://127.0.0.1:$GWAY_PORT/" \
+ // "404 Not Found"
+
+ // test_hostname_gateway_response_should_contain \
+ // "request for {dnslink-fqdn}/ipfs/{cid} returns 404 when path is not whitelisted" \
+ // "$NO_DNSLINK_FQDN" \
+ // "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv0" \
+ // "404 Not Found"
+
+ // ## ============================================================================
+ // ## Test wildcard DNSLink (any hostname, with default config)
+ // ## ============================================================================
+
+ // test_kill_ipfs_daemon
+
+ // # enable wildcard DNSLink gateway (any value in Host header)
+ // # and remove custom PublicGateways
+ // ipfs config --json Gateway.NoDNSLink false && \
+ // ipfs config --json Gateway.PublicGateways '{}' || exit 1
+
+ // # DNSLink test requires a daemon in online mode with precached /ipns/ mapping
+ // DNSLINK_FQDN="wildcard-dnslink-not-in-config.example.com"
+ // export IPFS_NS_MAP="$DNSLINK_FQDN:/ipfs/$CIDv1"
+
+ // # restart daemon to apply config changes
+ // test_launch_ipfs_daemon
+
+ // # make sure test setup is valid (fail if CoreAPI is unable to resolve)
+ // test_expect_success "spoofed DNSLink record resolves in cli" "
+ // ipfs resolve /ipns/$DNSLINK_FQDN > result &&
+ // test_should_contain \"$CIDv1\" result &&
+ // ipfs cat /ipns/$DNSLINK_FQDN > result &&
+ // test_should_contain \"$CID_VAL\" result
+ // "
+
+ // # gateway test
+ //
+ // test_hostname_gateway_response_should_contain \
+ // "request for http://{dnslink-fqdn}/ (wildcard) returns expected payload" \
+ // "$DNSLINK_FQDN" \
+ // "http://127.0.0.1:$GWAY_PORT/" \
+ // "$CID_VAL"
+ }...)
}
+
+ RunIfSpecsAreEnabled(t, helpers.UnwrapSubdomainTests(t, tests), specs.SubdomainGateway, specs.DNSLinkResolver)
}
diff --git a/tooling/dnslink/dnslink.go b/tooling/dnslink/dnslink.go
index 2a8778ab9..481eaa872 100644
--- a/tooling/dnslink/dnslink.go
+++ b/tooling/dnslink/dnslink.go
@@ -4,27 +4,36 @@ import (
"fmt"
"os"
"path"
+ "strings"
"github.com/ipfs/gateway-conformance/tooling/fixtures"
"gopkg.in/yaml.v3"
)
-type DNSLinks struct {
+type ConfigFixture struct {
DNSLinks map[string]DNSLink `yaml:"dnslinks"`
}
type DNSLink struct {
Subdomain string `yaml:"subdomain"`
+ Domain string `yaml:"domain"`
Path string `yaml:"path"`
}
-func OpenDNSLink(absPath string) (*DNSLinks, error) {
+func InlineDNS(s string) string {
+ // See spec at https://github.com/ipfs/specs/blob/main/src/http-gateways/subdomain-gateway.md#host-request-header
+ // Every - is replaced with --
+ // Every . is replaced with -
+ return strings.ReplaceAll(strings.ReplaceAll(s, "-", "--"), ".", "-")
+}
+
+func OpenDNSLink(absPath string) (*ConfigFixture, error) {
data, err := os.ReadFile(absPath)
if err != nil {
return nil, err
}
- var dnsLinks DNSLinks
+ var dnsLinks ConfigFixture
err = yaml.Unmarshal(data, &dnsLinks)
if err != nil {
return nil, err
@@ -33,7 +42,7 @@ func OpenDNSLink(absPath string) (*DNSLinks, error) {
return &dnsLinks, nil
}
-func MustOpenDNSLink(file string) *DNSLinks {
+func MustOpenDNSLink(file string) *ConfigFixture {
fixturePath := path.Join(fixtures.Dir(), file)
dnsLinks, err := OpenDNSLink(fixturePath)
if err != nil {
@@ -43,10 +52,24 @@ func MustOpenDNSLink(file string) *DNSLinks {
return dnsLinks
}
-func (d *DNSLinks) MustGet(id string) string {
+func (d *ConfigFixture) MustGet(id string) string {
dnsLink, ok := d.DNSLinks[id]
if !ok {
panic(fmt.Errorf("dnslink %s not found", id))
}
+ if dnsLink.Domain != "" && dnsLink.Subdomain != "" {
+ panic(fmt.Errorf("dnslink %s has both domain and subdomain", id))
+ }
+ if dnsLink.Domain == "" && dnsLink.Subdomain == "" {
+ panic(fmt.Errorf("dnslink %s has neither domain nor subdomain", id))
+ }
+ if dnsLink.Path == "" {
+ panic(fmt.Errorf("dnslink %s has no path", id))
+ }
+
+ if dnsLink.Domain != "" {
+ return dnsLink.Domain
+ }
+
return dnsLink.Subdomain
}
diff --git a/tooling/dnslink/merge.go b/tooling/dnslink/merge.go
index e1fb70b6c..446af9b19 100644
--- a/tooling/dnslink/merge.go
+++ b/tooling/dnslink/merge.go
@@ -6,8 +6,16 @@ import (
"os"
)
-func Aggregate(inputPaths []string) (map[string]string, error) {
- aggMap := make(map[string]string)
+type DNSLinksAggregate struct {
+ Domains map[string]string `json:"domains"`
+ Subdomains map[string]string `json:"subdomains"`
+}
+
+func Aggregate(inputPaths []string) (*DNSLinksAggregate, error) {
+ agg := DNSLinksAggregate{
+ Domains: make(map[string]string),
+ Subdomains: make(map[string]string),
+ }
for _, file := range inputPaths {
dnsLinks, err := OpenDNSLink(file)
@@ -16,15 +24,31 @@ func Aggregate(inputPaths []string) (map[string]string, error) {
}
for _, link := range dnsLinks.DNSLinks {
- if _, ok := aggMap[link.Subdomain]; ok {
- return nil, fmt.Errorf("collision detected for subdomain %s", link.Subdomain)
+ if link.Domain != "" && link.Subdomain != "" {
+ return nil, fmt.Errorf("dnslink %s has both domain and subdomain", link.Subdomain)
+ }
+
+ if link.Domain != "" {
+ if _, ok := agg.Domains[link.Domain]; ok {
+ return nil, fmt.Errorf("collision detected for domain %s", link.Domain)
+ }
+
+ agg.Domains[link.Domain] = link.Path
+ continue
}
- aggMap[link.Subdomain] = link.Path
+ if link.Subdomain != "" {
+ if _, ok := agg.Subdomains[link.Subdomain]; ok {
+ return nil, fmt.Errorf("collision detected for subdomain %s", link.Subdomain)
+ }
+
+ agg.Subdomains[link.Subdomain] = link.Path
+ continue
+ }
}
}
- return aggMap, nil
+ return &agg, nil
}
func Merge(inputPaths []string, outputPath string) error {
diff --git a/tooling/ipns/_fixtures/12D3KooWLQzUv2FHWGVPXTXSZpdHs7oHbXub2G5WC8Tx4NQhyd2d.ipns-record b/tooling/ipns/_fixtures/12D3KooWLQzUv2FHWGVPXTXSZpdHs7oHbXub2G5WC8Tx4NQhyd2d.ipns-record
new file mode 100644
index 000000000..39b2f41a4
Binary files /dev/null and b/tooling/ipns/_fixtures/12D3KooWLQzUv2FHWGVPXTXSZpdHs7oHbXub2G5WC8Tx4NQhyd2d.ipns-record differ
diff --git a/tooling/ipns/_fixtures/QmVujd5Vb7moysJj8itnGufN7MEtPRCNHkKpNuA4onsRa3.ipns-record b/tooling/ipns/_fixtures/QmVujd5Vb7moysJj8itnGufN7MEtPRCNHkKpNuA4onsRa3.ipns-record
new file mode 100644
index 000000000..b37d9b75b
Binary files /dev/null and b/tooling/ipns/_fixtures/QmVujd5Vb7moysJj8itnGufN7MEtPRCNHkKpNuA4onsRa3.ipns-record differ
diff --git a/tooling/ipns/ipns_test.go b/tooling/ipns/ipns_test.go
index c1b33f73a..f84265d8b 100644
--- a/tooling/ipns/ipns_test.go
+++ b/tooling/ipns/ipns_test.go
@@ -4,6 +4,8 @@ import (
"testing"
"time"
+ mbase "github.com/multiformats/go-multibase"
+ "github.com/multiformats/go-multicodec"
"github.com/stretchr/testify/assert"
)
@@ -62,3 +64,30 @@ func TestLoadTestRecord(t *testing.T) {
err = ipns.Valid()
assert.NoError(t, err)
}
+
+func TestIPNSFixtureVersionsConversion(t *testing.T) {
+ path := "./_fixtures/12D3KooWLQzUv2FHWGVPXTXSZpdHs7oHbXub2G5WC8Tx4NQhyd2d.ipns-record"
+ record, err := OpenIPNSRecordWithKey(path)
+
+ assert.Nil(t, err)
+
+ // 12D3KooWLQzUv2FHWGVPXTXSZpdHs7oHbXub2G5WC8Tx4NQhyd2d is a ED25519 key, which is using the identity hash.
+ assert.Equal(t, "12D3KooWLQzUv2FHWGVPXTXSZpdHs7oHbXub2G5WC8Tx4NQhyd2d", record.Key())
+ assert.Equal(t, "12D3KooWLQzUv2FHWGVPXTXSZpdHs7oHbXub2G5WC8Tx4NQhyd2d", record.IdV0())
+ assert.Equal(t, "k51qzi5uqu5dk3v4rmjber23h16xnr23bsggmqqil9z2gduiis5se8dht36dam", record.IdV1())
+ assert.Equal(t, "k50rm9yjlt0jey4fqg6wafvqprktgbkpgkqdg27tpqje6iimzxewnhvtin9hhq", record.ToCID(multicodec.DagPb, mbase.Base36))
+ assert.Equal(t, "12D3KooWLQzUv2FHWGVPXTXSZpdHs7oHbXub2G5WC8Tx4NQhyd2d", record.B58MH())
+ assert.Equal(t, "k51qzi5uqu5dk3v4rmjber23h16xnr23bsggmqqil9z2gduiis5se8dht36dam", record.ToCID(multicodec.Libp2pKey, mbase.Base36))
+
+ path = "./_fixtures/QmVujd5Vb7moysJj8itnGufN7MEtPRCNHkKpNuA4onsRa3.ipns-record"
+ record, err = OpenIPNSRecordWithKey(path)
+
+ assert.Nil(t, err)
+
+ // QmVujd5Vb7moysJj8itnGufN7MEtPRCNHkKpNuA4onsRa3 is a RSA key, which is using sha256 hash.
+ assert.Equal(t, "QmVujd5Vb7moysJj8itnGufN7MEtPRCNHkKpNuA4onsRa3", record.Key())
+ assert.Equal(t, "QmVujd5Vb7moysJj8itnGufN7MEtPRCNHkKpNuA4onsRa3", record.IdV0())
+ assert.Equal(t, "k2k4r8m7xvggw5pxxk3abrkwyer625hg01hfyggrai7lk1m63fuihi7w", record.IdV1())
+ assert.Equal(t, "k2jmtxu61bnhrtj301lw7zizknztocdbeqhxgv76l2q9t36fn9jbzipo", record.ToCID(multicodec.DagPb, mbase.Base36))
+ assert.Equal(t, "QmVujd5Vb7moysJj8itnGufN7MEtPRCNHkKpNuA4onsRa3", record.B58MH())
+}
diff --git a/tooling/ipns/record.go b/tooling/ipns/record.go
index 4132711ab..31ed5f059 100644
--- a/tooling/ipns/record.go
+++ b/tooling/ipns/record.go
@@ -1,16 +1,21 @@
package ipns
import (
+ "strings"
"time"
"github.com/ipfs/boxo/ipns"
ipns_pb "github.com/ipfs/boxo/ipns/pb"
+ "github.com/ipfs/go-cid"
"github.com/libp2p/go-libp2p/core/peer"
+ mbase "github.com/multiformats/go-multibase"
+ "github.com/multiformats/go-multicodec"
)
type IpnsRecord struct {
pb *ipns_pb.IpnsEntry
key string
+ id peer.ID
validity time.Time
}
@@ -25,7 +30,12 @@ func UnmarshalIpnsRecord(data []byte, pubKey string) (*IpnsRecord, error) {
return nil, err
}
- return &IpnsRecord{pb: pb, key: pubKey, validity: validity}, nil
+ id, err := peer.Decode(pubKey)
+ if err != nil {
+ return nil, err
+ }
+
+ return &IpnsRecord{pb: pb, key: pubKey, id: id, validity: validity}, nil
}
func (i *IpnsRecord) Value() string {
@@ -41,10 +51,39 @@ func (i *IpnsRecord) Validity() time.Time {
}
func (i *IpnsRecord) Valid() error {
- id, err := peer.Decode(i.key)
+ return ipns.ValidateWithPeerID(i.id, i.pb)
+}
+
+func (i *IpnsRecord) idV1(codec multicodec.Code, base mbase.Encoding) (string, error) {
+ c := peer.ToCid(i.id)
+ c = cid.NewCidV1(uint64(codec), c.Hash())
+ s, err := c.StringOfBase(base)
if err != nil {
- return err
+ return "", err
}
+ return s, nil
+}
+
+func (i *IpnsRecord) ToCID(codec multicodec.Code, base mbase.Encoding) string {
+ s, err := i.idV1(codec, base)
+ if err != nil {
+ panic(err)
+ }
+ return s
+}
+
+func (i *IpnsRecord) IdV0() string {
+ if strings.HasPrefix(i.key, "Qm") || strings.HasPrefix(i.key, "1") {
+ return i.key
+ }
+
+ panic("not a v0 id")
+}
+
+func (i *IpnsRecord) IdV1() string {
+ return i.ToCID(cid.Libp2pKey, mbase.Base36)
+}
- return ipns.ValidateWithPeerID(id, i.pb)
+func (i *IpnsRecord) B58MH() string {
+ return i.id.String()
}