Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.11.1] - 2026-02-24
### Added
- New test `TestGatewayIPNSRecordWithSubpath` for IPNS records whose Value field contains a sub-path (e.g. `/ipfs/<cid>/root2`). All prior IPNS tests only covered bare `/ipfs/<cid>` in the record Value.
- New test `TestDNSLinkGatewayWithSubpath` for DNSLink TXT records pointing at a content path with a sub-path (e.g. `dnslink=/ipfs/<cid>/root2`).

## [0.11.0] - 2026-02-24
### Added
- New test `TestDNSLinkGatewayIPNS` for DNSLink TXT records pointing at `/ipns/<key>` instead of `/ipfs/<cid>`. All prior DNSLink tests only covered the `/ipfs/` case. [#272](https://github.com/ipfs/gateway-conformance/pull/272)
Expand Down
13 changes: 13 additions & 0 deletions fixtures/gateway-cache/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,16 @@ ipfs dag export ${ROOT1_CID} > ./fixtures.car
# FILE_CID=bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am # ./root2/root3/root4/index.html
# TEST_IPNS_ID=k51qzi5uqu5dlxdsdu5fpuu7h69wu4ohp32iwm9pdt9nq3y5rpn3ln9j12zfhe
```

### ipns-record with sub-path in Value

IPNS record whose Value points at `/ipfs/ROOT1_CID/root2` instead of just
`/ipfs/ROOT1_CID`. Used to test that gateways correctly handle IPNS records
with sub-paths embedded in the Value field.

Generated by `makeSubpathRecord()` in `../ipns_records/generator/main.go`
(run with `cd ../ipns_records && go run generator/main.go`).

```
# SUBPATH_IPNS_ID=k51qzi5uqu5djokp3m1keo36hoxtd6u3a1d2rg1camf6al7p3huy63dojlm57c
```
6 changes: 6 additions & 0 deletions fixtures/gateway-cache/dnslink.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# yaml-language-server: $schema=../fixture.schema.json
dnslinks:
dnslink-with-subpath:
domain: dnslink-subpath.example.org
# ROOT1_CID from gateway-cache/fixtures.car with /root2 sub-path
path: /ipfs/bafybeib3ffl2teiqdncv3mkz4r23b5ctrwkzrrhctdbne6iboayxuxk5ui/root2
Binary file not shown.
23 changes: 23 additions & 0 deletions fixtures/ipns_records/generator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,34 @@ func makeV2Only() {
saveToFile(raw, name.String()+"_v2.ipns-record", v)
}

// makeSubpathRecord creates a valid V1+V2 IPNS record whose Value contains a
// sub-path (e.g. /ipfs/<cid>/root2) instead of a bare CID. This record is
// placed in gateway-cache/ alongside the CAR it references.
func makeSubpathRecord() {
// ROOT1_CID from gateway-cache/fixtures.car (see ../gateway-cache/README.md)
root1CID, err := cid.Decode("bafybeib3ffl2teiqdncv3mkz4r23b5ctrwkzrrhctdbne6iboayxuxk5ui")
panicOnErr(err)

v, err := path.Join(path.FromCid(root1CID), "root2")
panicOnErr(err)

sk, _, name := makeKeyPair()

rec, err := ipns.NewRecord(sk, v, seq, eol, ttl, ipns.WithV1Compatibility(true))
panicOnErr(err)

raw, err := ipns.MarshalRecord(rec)
panicOnErr(err)

saveToFile(raw, "../gateway-cache/"+name.String()+".ipns-record", v)
}

func main() {
makeV1Only()
makeV1V2()
makeV1V2WithBrokenValue()
makeV1V2WithBrokenSignatureV1()
makeV1V2WithBrokenSignatureV2()
makeV2Only()
makeSubpathRecord()
}
26 changes: 26 additions & 0 deletions tests/dnslink_gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,29 @@ func TestDNSLinkGatewayUnixFSDirectoryListing(t *testing.T) {

RunWithSpecs(t, tests, specs.DNSLinkGateway)
}

func TestDNSLinkGatewayWithSubpath(t *testing.T) {
tooling.LogTestGroup(t, GroupDNSLink)

dnsLinks := dnslink.MustOpenDNSLink("gateway-cache/dnslink.yml")
dnsLink := dnsLinks.MustGet("dnslink-with-subpath")

tests := SugarTests{
{
Name: "GET for DNSLink with dnslink=/ipfs/<cid>/sub-path returns file at sub-path",
Hint: `
When a DNSLink TXT record points to a content path with a sub-path
(e.g. /ipfs/<cid>/root2), the gateway must resolve the full path and
concatenate any additional request path segments to serve the file.
`,
Request: Request().
Header("Host", dnsLink).
Path("/root3/root4/index.html"),
Response: Expect().
Status(200).
Body("hello\n"),
},
}

RunWithSpecs(t, tests, specs.DNSLinkGateway)
}
26 changes: 26 additions & 0 deletions tests/path_gateway_ipns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,29 @@ func TestRedirectCanonicalIPNS(t *testing.T) {

RunWithSpecs(t, tests, specs.PathGatewayIPNS)
}

func TestGatewayIPNSRecordWithSubpath(t *testing.T) {
Copy link
Member Author

@lidel lidel Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explainer for posterity/llms (this is about IPNS, but similar applies to DNSLink one)


TestGatewayIPNSRecordWithSubpath

Verifies that a gateway correctly resolves IPNS records whose Value field
contains a sub-path (e.g. /ipfs/<cid>/sub/path) rather than a bare CID.

Shared fixture: gateway-cache/fixtures.car

A CARv1 file containing a UnixFS directory tree:

bafybeib3ffl2teiqdncv3mkz4r23b5ctrwkzrrhctdbne6iboayxuxk5ui   (ROOT1_CID) ./
└── root2/
    └── root3/
        └── root4/
            └── index.html  ->  "hello\n"

Key CIDs:

Alias CID Path
ROOT1_CID bafybeib3ffl2teiqdncv3mkz4r23b5ctrwkzrrhctdbne6iboayxuxk5ui ./
ROOT2_CID bafybeih2w7hjocxjg6g2ku25hvmd53zj7og4txpby3vsusfefw5rrg5sii ./root2
ROOT3_CID bafybeiawdvhmjcz65x5egzx4iukxc72hg4woks6v6fvgyupiyt3oczk5ja ./root2/root3
ROOT4_CID bafybeifq2rzpqnqrsdupncmkmhs3ckxxjhuvdcbvydkgvch3ms24k5lo7q ./root2/root3/root4
FILE_CID bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am ./root2/root3/root4/index.html

IPNS record

Generated by makeSubpathRecord() in fixtures/ipns_records/generator/main.go.
It creates a valid V1+V2 IPNS record with a random Ed25519 key pair. The record's
Value field points at ROOT1_CID with a sub-path appended:

Value = /ipfs/bafybeib3ffl2teiqdncv3mkz4r23b5ctrwkzrrhctdbne6iboayxuxk5ui/root2

The resulting IPNS key (base36):

SUBPATH_IPNS_ID = k51qzi5uqu5djokp3m1keo36hoxtd6u3a1d2rg1camf6al7p3huy63dojlm57c

The signed record is committed as a binary fixture at
fixtures/gateway-cache/k51qzi5uqu5djokp3m1keo36hoxtd6u3a1d2rg1camf6al7p3huy63dojlm57c.ipns-record.

Test request

GET /ipns/k51qzi5uqu5djokp3m1keo36hoxtd6u3a1d2rg1camf6al7p3huy63dojlm57c/root3/root4/index.html

Resolution steps

  1. Gateway parses the request path:

    • namespace: /ipns/
    • IPNS key: k51qzi5uqu5djokp3m1keo36hoxtd6u3a1d2rg1camf6al7p3huy63dojlm57c
    • remaining path: /root3/root4/index.html
  2. Gateway fetches and validates the IPNS record for that key.

  3. Extracts the Value from the record:
    /ipfs/bafybeib3ffl2teiqdncv3mkz4r23b5ctrwkzrrhctdbne6iboayxuxk5ui/root2

  4. Concatenates the remaining request path onto the resolved Value:

    /ipfs/bafybeib3ffl2teiqdncv3mkz4r23b5ctrwkzrrhctdbne6iboayxuxk5ui/root2 + /root3/root4/index.html
    

    Final content path:

    /ipfs/bafybeib3ffl2teiqdncv3mkz4r23b5ctrwkzrrhctdbne6iboayxuxk5ui/root2/root3/root4/index.html
    
  5. Resolves through the UnixFS DAG in fixtures.car:
    ROOT1_CID -> root2/ -> root3/ -> root4/ -> index.html

  6. Expected response: 200 with body hello\n

Why this matters

All prior IPNS tests used records whose Value was a bare /ipfs/<cid> with no
trailing path segments. This test covers the case where the IPNS record itself
embeds a sub-path in its Value. Gateways must concatenate the request path
after the full resolved Value, not just after the CID.

tooling.LogTestGroup(t, GroupIPNS)

// IPNS record whose Value = /ipfs/ROOT1_CID/root2 (note the sub-path)
// reuses the gateway-cache/fixtures.car which has root2/root3/root4/index.html
ipnsSubpath := MustOpenIPNSRecordWithKey("gateway-cache/k51qzi5uqu5djokp3m1keo36hoxtd6u3a1d2rg1camf6al7p3huy63dojlm57c.ipns-record")

tests := SugarTests{
{
Name: "GET /ipns/{key}/root3/root4/index.html returns file when IPNS record Value has sub-path",
Hint: `
When an IPNS record Value contains a sub-path (e.g. /ipfs/<cid>/root2),
the gateway must concatenate the request path with the IPNS-resolved
content path and serve the resulting file.
`,
Request: Request().
Path("/ipns/{{key}}/root3/root4/index.html", ipnsSubpath.Key()),
Response: Expect().
Status(200).
Body("hello\n"),
},
}

RunWithSpecs(t, tests, specs.PathGatewayIPNS)
}
Loading