Skip to content

Commit

Permalink
refactor: cleanup isDNSLinkName mess
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed Feb 3, 2023
1 parent c091fd6 commit 13b6f58
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 141 deletions.
5 changes: 5 additions & 0 deletions examples/gateway-car/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ func (api *blocksGateway) ResolvePath(ctx context.Context, p ifacepath.Path) (if
return ifacepath.NewResolvedPath(ipath, node, root, gopath.Join(rest...)), nil
}

func (api *blocksGateway) HasDNSLinkRecord(ctx context.Context, host string) bool {
// Not implemented.
return false
}

func (api *blocksGateway) resolveNode(ctx context.Context, p ifacepath.Path) (format.Node, error) {
rp, err := api.ResolvePath(ctx, p)
if err != nil {
Expand Down
15 changes: 0 additions & 15 deletions examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/go-bitfield v1.0.0 // indirect
Expand All @@ -55,20 +52,12 @@ require (
github.com/ipfs/go-log v1.0.5 // indirect
github.com/ipfs/go-log/v2 v2.5.1 // indirect
github.com/ipfs/go-metrics-interface v0.0.1 // indirect
github.com/ipfs/go-namesys v0.7.0 // indirect
github.com/ipfs/go-verifcid v0.0.2 // indirect
github.com/ipld/go-car v0.5.0 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.3 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-cidranger v1.1.0 // indirect
github.com/libp2p/go-libp2p v0.23.4 // indirect
github.com/libp2p/go-libp2p-asn-util v0.2.0 // indirect
github.com/libp2p/go-libp2p-kad-dht v0.19.0 // indirect
github.com/libp2p/go-libp2p-kbucket v0.5.0 // indirect
github.com/libp2p/go-libp2p-record v0.2.0 // indirect
github.com/libp2p/go-msgio v0.2.0 // indirect
github.com/libp2p/go-netroute v0.2.0 // indirect
github.com/libp2p/go-openssl v0.1.0 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-pointer v0.0.1 // indirect
Expand All @@ -79,7 +68,6 @@ require (
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/multiformats/go-multiaddr v0.8.0 // indirect
github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect
github.com/multiformats/go-multibase v0.1.1 // indirect
github.com/multiformats/go-multicodec v0.7.0 // indirect
github.com/multiformats/go-multihash v0.2.1 // indirect
Expand All @@ -96,11 +84,8 @@ require (
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb // indirect
github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc // indirect
github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 // indirect
github.com/whyrusleeping/cbor-gen v0.0.0-20230126041949-52956bd4c9aa // indirect
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
go.opencensus.io v0.23.0 // indirect
go.opentelemetry.io/otel v1.12.0 // indirect
go.opentelemetry.io/otel/trace v1.12.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
Expand Down
43 changes: 0 additions & 43 deletions examples/go.sum

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ type API interface {
// exist due to a missing link, it should return an error of type:
// https://pkg.go.dev/github.com/ipfs/[email protected]/resolver#ErrNoLink
ResolvePath(context.Context, path.Path) (path.Resolved, error)

// HasDNSLinkRecord returns if the provided path has a DNSLink TXT record.
// It does not perform any validation, only checks for the existence of
// a DNSLink TXT record.
HasDNSLinkRecord(context.Context, string) bool
}

// A helper function to clean up a set of headers:
Expand Down
100 changes: 100 additions & 0 deletions gateway/gateway_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package gateway

import (
"context"
"errors"
"strings"

cid "github.com/ipfs/go-cid"
"github.com/ipfs/go-libipfs/blocks"
"github.com/ipfs/go-libipfs/files"
"github.com/ipfs/go-namesys"
path "github.com/ipfs/go-path"
iface "github.com/ipfs/interface-go-ipfs-core"
nsopts "github.com/ipfs/interface-go-ipfs-core/options/namesys"
ipath "github.com/ipfs/interface-go-ipfs-core/path"
"github.com/libp2p/go-libp2p/core/crypto"
)

type mockNamesys map[string]path.Path

func (m mockNamesys) Resolve(ctx context.Context, name string, opts ...nsopts.ResolveOpt) (value path.Path, err error) {
cfg := nsopts.DefaultResolveOpts()
for _, o := range opts {
o(&cfg)
}
depth := cfg.Depth
if depth == nsopts.UnlimitedDepth {
// max uint
depth = ^uint(0)
}
for strings.HasPrefix(name, "/ipns/") {
if depth == 0 {
return value, namesys.ErrResolveRecursion
}
depth--

var ok bool
value, ok = m[name]
if !ok {
return "", namesys.ErrResolveFailed
}
name = value.String()
}
return value, nil
}

func (m mockNamesys) ResolveAsync(ctx context.Context, name string, opts ...nsopts.ResolveOpt) <-chan namesys.Result {
out := make(chan namesys.Result, 1)
v, err := m.Resolve(ctx, name, opts...)
out <- namesys.Result{Path: v, Err: err}
close(out)
return out
}

func (m mockNamesys) Publish(ctx context.Context, name crypto.PrivKey, value path.Path, opts ...nsopts.PublishOption) error {
return errors.New("not implemented for mockNamesys")
}

func (m mockNamesys) GetResolver(subs string) (namesys.Resolver, bool) {
return nil, false
}

type mockApi struct {
ns mockNamesys
}

func newMockApi() *mockApi {
return &mockApi{
ns: mockNamesys{},
}
}

func (m *mockApi) GetUnixFsNode(context.Context, ipath.Resolved) (files.Node, error) {
return nil, errors.New("not implemented")
}

func (m *mockApi) LsUnixFsDir(context.Context, ipath.Resolved) (<-chan iface.DirEntry, error) {
return nil, errors.New("not implemented")
}

func (m *mockApi) GetBlock(context.Context, cid.Cid) (blocks.Block, error) {
return nil, errors.New("not implemented")
}

func (m *mockApi) GetIPNSRecord(context.Context, cid.Cid) ([]byte, error) {
return nil, errors.New("not implemented")
}

func (m *mockApi) IsCached(context.Context, ipath.Path) bool {
return false
}

func (m *mockApi) ResolvePath(context.Context, ipath.Path) (ipath.Resolved, error) {
return nil, errors.New("not implemented")
}

func (m *mockApi) HasDNSLinkRecord(ctx context.Context, hostname string) bool {
_, err := m.ns.Resolve(ctx, "/ipns/"+hostname, nsopts.Depth(1))
return err == nil || err == namesys.ErrResolveRecursion
}
23 changes: 9 additions & 14 deletions gateway/hostname.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import (
"strings"

cid "github.com/ipfs/go-cid"
"github.com/ipfs/go-namesys"
"github.com/ipfs/interface-go-ipfs-core/path"
"github.com/libp2p/go-libp2p/core/peer"
dns "github.com/miekg/dns"

Expand Down Expand Up @@ -113,7 +111,7 @@ func WithHostname(next http.Handler, api API, publicGateways map[string]*Specifi
// Not a whitelisted path

// Try DNSLink, if it was not explicitly disabled for the hostname
if !gw.NoDNSLink && isDNSLinkName(r.Context(), api, host) {
if !gw.NoDNSLink && hasDNSLinkRecord(r.Context(), api, host) {
// rewrite path and handle as DNSLink
r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path
next.ServeHTTP(w, withHostnameContext(r, host))
Expand Down Expand Up @@ -184,7 +182,6 @@ func WithHostname(next http.Handler, api API, publicGateways map[string]*Specifi
}
}
} else { // rootID is not a CID..

// Check if rootID is a single DNS label with an inlined
// DNSLink FQDN a single DNS label. We support this so
// loading DNSLink names over TLS "just works" on public
Expand All @@ -200,10 +197,10 @@ func WithHostname(next http.Handler, api API, publicGateways map[string]*Specifi
// https://my-v--long-example-com.ipns.dweb.link
if ns == "ipns" && !strings.Contains(rootID, ".") {
// if there is no TXT recordfor rootID
if !isDNSLinkName(r.Context(), api, rootID) {
if !hasDNSLinkRecord(r.Context(), api, rootID) {
// my-v--long-example-com → my.v-long.example.com
dnslinkFQDN := toDNSLinkFQDN(rootID)
if isDNSLinkName(r.Context(), api, dnslinkFQDN) {
if hasDNSLinkRecord(r.Context(), api, dnslinkFQDN) {
// update path prefix to use real FQDN with DNSLink
pathPrefix = "/ipns/" + dnslinkFQDN
}
Expand All @@ -218,13 +215,14 @@ func WithHostname(next http.Handler, api API, publicGateways map[string]*Specifi
next.ServeHTTP(w, withHostnameContext(r, gwHostname))
return
}

// We don't have a known gateway. Fallback on DNSLink lookup

// Wildcard HTTP Host check:
// 1. is wildcard DNSLink enabled (Gateway.NoDNSLink=false)?
// 2. does Host header include a fully qualified domain name (FQDN)?
// 3. does DNSLink record exist in DNS?
if !noDNSLink && isDNSLinkName(r.Context(), api, host) {
if !noDNSLink && hasDNSLinkRecord(r.Context(), api, host) {
// rewrite path and handle as DNSLink
r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path
ctx := context.WithValue(r.Context(), DNSLinkHostnameKey, host)
Expand Down Expand Up @@ -262,18 +260,15 @@ func isDomainNameAndNotPeerID(hostname string) bool {
return ok
}

// isDNSLinkName returns bool if a valid DNS TXT record exist for provided host
func isDNSLinkName(ctx context.Context, api API, host string) bool {
// hasDNSLinkRecord returns if a DNS TXT record exists for the provided host.
func hasDNSLinkRecord(ctx context.Context, api API, host string) bool {
dnslinkName := stripPort(host)

if !isDomainNameAndNotPeerID(dnslinkName) {
return false
}

name := "/ipns/" + dnslinkName
_, err := api.ResolvePath(ctx, path.New(name))
// check if DNSLink exists
return err == nil || err == namesys.ErrResolveRecursion
return api.HasDNSLinkRecord(ctx, dnslinkName)
}

func isSubdomainNamespace(ns string) bool {
Expand Down Expand Up @@ -456,7 +451,7 @@ func toSubdomainURL(hostname, path string, r *http.Request, inlineDNSLink bool,
// represented as a single DNS label:
// https://my-v--long-example-com.ipns.dweb.link
if (inlineDNSLink || isHTTPS) && ns == "ipns" && strings.Contains(rootID, ".") {
if isDNSLinkName(r.Context(), api, rootID) {
if hasDNSLinkRecord(r.Context(), api, rootID) {
// my.v-long.example.com → my-v--long-example-com
dnsLabel, err := toDNSLinkDNSLabel(rootID)
if err != nil {
Expand Down
121 changes: 52 additions & 69 deletions gateway/hostname_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,80 +7,63 @@ import (
"testing"

cid "github.com/ipfs/go-cid"
path "github.com/ipfs/go-path"
)

// import (
// "errors"
// "net/http"
// "net/http/httptest"
// "testing"
func TestToSubdomainURL(t *testing.T) {
gwAPI := newMockApi()
testCID, err := cid.Decode("bafkqaglimvwgy3zakrsxg5cun5jxkyten5wwc2lokvjeycq")
if err != nil {
t.Fatal(err)
}

gwAPI.ns["/ipns/dnslink.long-name.example.com"] = path.FromString(testCID.String())
gwAPI.ns["/ipns/dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com"] = path.FromString(testCID.String())
httpRequest := httptest.NewRequest("GET", "http://127.0.0.1:8080", nil)
httpsRequest := httptest.NewRequest("GET", "https://https-request-stub.example.com", nil)
httpsProxiedRequest := httptest.NewRequest("GET", "http://proxied-https-request-stub.example.com", nil)
httpsProxiedRequest.Header.Set("X-Forwarded-Proto", "https")

// cid "github.com/ipfs/go-cid"
// "github.com/ipfs/go-libipfs/files"
// path "github.com/ipfs/go-path"
// config "github.com/ipfs/kubo/config"
// coreapi "github.com/ipfs/kubo/core/coreapi"
// )
for _, test := range []struct {
// in:
request *http.Request
gwHostname string
inlineDNSLink bool
path string
// out:
url string
err error
}{

// func TestToSubdomainURL(t *testing.T) {
// ns := mockNamesys{}
// n, err := newNodeWithMockNamesys(ns)
// if err != nil {
// t.Fatal(err)
// }
// coreAPI, err := coreapi.NewCoreAPI(n)
// if err != nil {
// t.Fatal(err)
// }
// testCID, err := coreAPI.Unixfs().Add(n.Context(), files.NewBytesFile([]byte("fnord")))
// if err != nil {
// t.Fatal(err)
// }
// ns["/ipns/dnslink.long-name.example.com"] = path.FromString(testCID.String())
// ns["/ipns/dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com"] = path.FromString(testCID.String())
// httpRequest := httptest.NewRequest("GET", "http://127.0.0.1:8080", nil)
// httpsRequest := httptest.NewRequest("GET", "https://https-request-stub.example.com", nil)
// httpsProxiedRequest := httptest.NewRequest("GET", "http://proxied-https-request-stub.example.com", nil)
// httpsProxiedRequest.Header.Set("X-Forwarded-Proto", "https")
// DNSLink
{httpRequest, "localhost", false, "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", nil},
// Hostname with port
{httpRequest, "localhost:8080", false, "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", nil},
// CIDv0 → CIDv1base32
{httpRequest, "localhost", false, "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", nil},
// CIDv1 with long sha512
{httpRequest, "localhost", false, "/ipfs/bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
// PeerID as CIDv1 needs to have libp2p-key multicodec
{httpRequest, "localhost", false, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://k2k4r8n0flx3ra0y5dr8fmyvwbzy3eiztmtq6th694k5a3rznayp3e4o.ipns.localhost/", nil},
{httpRequest, "localhost", false, "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://k2k4r8l9ja7hkzynavdqup76ou46tnvuaqegbd04a4o1mpbsey0meucb.ipns.localhost/", nil},
// PeerID: ed25519+identity multihash → CIDv1Base36
{httpRequest, "localhost", false, "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://k51qzi5uqu5di608geewp3nqkg0bpujoasmka7ftkyxgcm3fh1aroup0gsdrna.ipns.localhost/", nil},
{httpRequest, "sub.localhost", false, "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.sub.localhost/", nil},
// HTTPS requires DNSLink name to fit in a single DNS label – see "Option C" from https://github.com/ipfs/in-web-browsers/issues/169
{httpRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "http://dnslink.long-name.example.com.ipns.dweb.link/", nil},
{httpsRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
{httpsProxiedRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
// HTTP requests can also be converted to fit into a single DNS label - https://github.com/ipfs/kubo/issues/9243
{httpRequest, "localhost", true, "/ipns/dnslink.long-name.example.com", "http://dnslink-long--name-example-com.ipns.localhost/", nil},
{httpRequest, "dweb.link", true, "/ipns/dnslink.long-name.example.com", "http://dnslink-long--name-example-com.ipns.dweb.link/", nil},
} {

// for _, test := range []struct {
// // in:
// request *http.Request
// gwHostname string
// inlineDNSLink bool
// path string
// // out:
// url string
// err error
// }{
// // DNSLink
// {httpRequest, "localhost", false, "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", nil},
// // Hostname with port
// {httpRequest, "localhost:8080", false, "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", nil},
// // CIDv0 → CIDv1base32
// {httpRequest, "localhost", false, "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", nil},
// // CIDv1 with long sha512
// {httpRequest, "localhost", false, "/ipfs/bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
// // PeerID as CIDv1 needs to have libp2p-key multicodec
// {httpRequest, "localhost", false, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://k2k4r8n0flx3ra0y5dr8fmyvwbzy3eiztmtq6th694k5a3rznayp3e4o.ipns.localhost/", nil},
// {httpRequest, "localhost", false, "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://k2k4r8l9ja7hkzynavdqup76ou46tnvuaqegbd04a4o1mpbsey0meucb.ipns.localhost/", nil},
// // PeerID: ed25519+identity multihash → CIDv1Base36
// {httpRequest, "localhost", false, "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://k51qzi5uqu5di608geewp3nqkg0bpujoasmka7ftkyxgcm3fh1aroup0gsdrna.ipns.localhost/", nil},
// {httpRequest, "sub.localhost", false, "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.sub.localhost/", nil},
// // HTTPS requires DNSLink name to fit in a single DNS label – see "Option C" from https://github.com/ipfs/in-web-browsers/issues/169
// {httpRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "http://dnslink.long-name.example.com.ipns.dweb.link/", nil},
// {httpsRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
// {httpsProxiedRequest, "dweb.link", false, "/ipns/dnslink.long-name.example.com", "https://dnslink-long--name-example-com.ipns.dweb.link/", nil},
// // HTTP requests can also be converted to fit into a single DNS label - https://github.com/ipfs/kubo/issues/9243
// {httpRequest, "localhost", true, "/ipns/dnslink.long-name.example.com", "http://dnslink-long--name-example-com.ipns.localhost/", nil},
// {httpRequest, "dweb.link", true, "/ipns/dnslink.long-name.example.com", "http://dnslink-long--name-example-com.ipns.dweb.link/", nil},
// } {
// url, err := toSubdomainURL(test.gwHostname, test.path, test.request, test.inlineDNSLink, coreAPI)
// if url != test.url || !equalError(err, test.err) {
// t.Errorf("(%s, %v, %s) returned (%s, %v), expected (%s, %v)", test.gwHostname, test.inlineDNSLink, test.path, url, err, test.url, test.err)
// }
// }
// }
url, err := toSubdomainURL(test.gwHostname, test.path, test.request, test.inlineDNSLink, gwAPI)
if url != test.url || !equalError(err, test.err) {
t.Errorf("(%s, %v, %s) returned (%s, %v), expected (%s, %v)", test.gwHostname, test.inlineDNSLink, test.path, url, err, test.url, test.err)
}
}
}

func TestToDNSLinkDNSLabel(t *testing.T) {
for _, test := range []struct {
Expand Down

0 comments on commit 13b6f58

Please sign in to comment.