Skip to content

Commit 0f7d1af

Browse files
committed
feat(gateway)!: new trustless mode, and by default
1 parent aa12ba3 commit 0f7d1af

File tree

9 files changed

+394
-85
lines changed

9 files changed

+394
-85
lines changed

examples/gateway/common/handler.go

+36-24
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,46 @@ import (
1010
)
1111

1212
func NewHandler(gwAPI gateway.IPFSBackend) http.Handler {
13-
// Initialize the headers and gateway configuration. For this example, we do
14-
// not add any special headers, but the required ones.
15-
headers := map[string][]string{}
16-
gateway.AddAccessControlHeaders(headers)
1713
conf := gateway.Config{
18-
Headers: headers,
19-
}
14+
// Initialize the headers. For this example, we do not add any special headers,
15+
// only the required ones via gateway.AddAccessControlHeaders.
16+
Headers: map[string][]string{},
2017

21-
// Initialize the public gateways that we will want to have available through
22-
// Host header rewriting. This step is optional and only required if you're
23-
// running multiple public gateways and want different settings and support
24-
// for DNSLink and Subdomain Gateways.
25-
noDNSLink := false // If you set DNSLink to point at the CID from CAR, you can load it!
26-
publicGateways := map[string]*gateway.Specification{
27-
// Support public requests with Host: CID.ipfs.example.net and ID.ipns.example.net
28-
"example.net": {
29-
Paths: []string{"/ipfs", "/ipns"},
30-
NoDNSLink: noDNSLink,
31-
UseSubdomains: true,
32-
},
33-
// Support local requests
34-
"localhost": {
35-
Paths: []string{"/ipfs", "/ipns"},
36-
NoDNSLink: noDNSLink,
37-
UseSubdomains: true,
18+
// If you set DNSLink to point at the CID from CAR, you can load it!
19+
NoDNSLink: false,
20+
21+
// For these examples we have the trusted mode enabled by default. That is,
22+
// all types of requests will be accepted. By default, only Trustless Gateway
23+
// requests work: https://specs.ipfs.tech/http-gateways/trustless-gateway/
24+
TrustedMode: true,
25+
26+
// Initialize the public gateways that we will want to have available through
27+
// Host header rewriting. This step is optional and only required if you're
28+
// running multiple public gateways and want different settings and support
29+
// for DNSLink and Subdomain Gateways.
30+
PublicGateways: map[string]*gateway.Specification{
31+
// Support public requests with Host: CID.ipfs.example.net and ID.ipns.example.net
32+
"example.net": {
33+
Paths: []string{"/ipfs", "/ipns"},
34+
NoDNSLink: false,
35+
UseSubdomains: true,
36+
// This gateway is used for testing and therefore we make non-trustless
37+
// requests. Thus, we have to manually turn on the trusted mode.
38+
TrustedMode: true,
39+
},
40+
// Support local requests
41+
"localhost": {
42+
Paths: []string{"/ipfs", "/ipns"},
43+
NoDNSLink: false,
44+
UseSubdomains: true,
45+
TrustedMode: true,
46+
},
3847
},
3948
}
4049

50+
// Add required access control headers to the configuration.
51+
gateway.AddAccessControlHeaders(conf.Headers)
52+
4153
// Creates a mux to serve the gateway paths. This is not strictly necessary
4254
// and gwHandler could be used directly. However, on the next step we also want
4355
// to add prometheus metrics, hence needing the mux.
@@ -57,7 +69,7 @@ func NewHandler(gwAPI gateway.IPFSBackend) http.Handler {
5769
// or example.net. If you want to expose the metrics on such gateways,
5870
// you will have to add the path "/debug" to the variable Paths.
5971
var handler http.Handler
60-
handler = gateway.WithHostname(mux, gwAPI, publicGateways, noDNSLink)
72+
handler = gateway.WithHostname(conf, gwAPI, mux)
6173

6274
// Then, wrap with the withConnect middleware. This is required since we use
6375
// http.ServeMux which does not support CONNECT by default.

gateway/gateway.go

+67-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,62 @@ import (
1515

1616
// Config is the configuration used when creating a new gateway handler.
1717
type Config struct {
18+
// Headers is a map containing all the headers that should be sent by default
19+
// in all requests. You can define custom headers, as well as add the recommended
20+
// headers via AddAccessControlHeaders.
1821
Headers map[string][]string
22+
23+
// TrustedMode configures this gateway to allow trusted requests. By default,
24+
// the gateway will operate in trustless mode, as defined in the specification:
25+
// https://specs.ipfs.tech/http-gateways/trustless-gateway/.
26+
//
27+
// This only applies to hostnames not defined under PublicGateways. In addition,
28+
// the hostnames localhost, 127.0.0.1 and ::1 are considered trusted by default.
29+
TrustedMode bool
30+
31+
// NoDNSLink configures the gateway to _not_ perform DNS TXT record lookups in
32+
// response to requests with values in `Host` HTTP header. This flag can be
33+
// overridden per FQDN in PublicGateways. To be used with WithHostname.
34+
NoDNSLink bool
35+
36+
// PublicGateways configures the behavior of known public gateways. Each key is
37+
// a fully qualified domain name (FQDN). To be used with WithHostname.
38+
PublicGateways map[string]*Specification
39+
}
40+
41+
// Specification is the specification of an IPFS Public Gateway.
42+
type Specification struct {
43+
// Paths is explicit list of path prefixes that should be handled by
44+
// this gateway. Example: `["/ipfs", "/ipns"]`
45+
// Useful if you only want to support immutable `/ipfs`.
46+
Paths []string
47+
48+
// UseSubdomains indicates whether or not this gateway uses subdomains
49+
// for IPFS resources instead of paths. That is: http://CID.ipfs.GATEWAY/...
50+
//
51+
// If this flag is set, any /ipns/$id and/or /ipfs/$id paths in Paths
52+
// will be permanently redirected to http://$id.[ipns|ipfs].$gateway/.
53+
//
54+
// We do not support using both paths and subdomains for a single domain
55+
// for security reasons (Origin isolation).
56+
UseSubdomains bool
57+
58+
// NoDNSLink configures this gateway to _not_ resolve DNSLink for the
59+
// specific FQDN provided in `Host` HTTP header. Useful when you want to
60+
// explicitly allow or refuse hosting a single hostname. To refuse all
61+
// DNSLinks in `Host` processing, set NoDNSLink in Config instead. This setting
62+
// overrides the global setting.
63+
NoDNSLink bool
64+
65+
// InlineDNSLink configures this gateway to always inline DNSLink names
66+
// (FQDN) into a single DNS label in order to interop with wildcard TLS certs
67+
// and Origin per CID isolation provided by rules like https://publicsuffix.org
68+
// This should be set to true if you use HTTPS.
69+
InlineDNSLink bool
70+
71+
// TrustedMode configures this gateway to allow trusted requests. This setting
72+
// overrides the global setting. Not setting TrustedMode enables Trustless Mode.
73+
TrustedMode bool
1974
}
2075

2176
// TODO: Is this what we want for ImmutablePath?
@@ -221,7 +276,17 @@ func AddAccessControlHeaders(headers map[string][]string) {
221276
type RequestContextKey string
222277

223278
const (
224-
DNSLinkHostnameKey RequestContextKey = "dnslink-hostname"
279+
// GatewayHostnameKey is the key for the hostname at which the gateway is
280+
// operating. It may be a DNSLink, Subdomain or Regular gateway.
225281
GatewayHostnameKey RequestContextKey = "gw-hostname"
226-
ContentPathKey RequestContextKey = "content-path"
282+
283+
// DNSLinkHostnameKey is the key for the hostname of a DNSLink Gateway:
284+
// https://specs.ipfs.tech/http-gateways/dnslink-gateway/
285+
DNSLinkHostnameKey RequestContextKey = "dnslink-hostname"
286+
287+
// SubdomainHostnameKey is the key for the hostname of a Subdomain Gateway:
288+
// https://specs.ipfs.tech/http-gateways/subdomain-gateway/
289+
SubdomainHostnameKey RequestContextKey = "subdomain-hostname"
290+
291+
ContentPathKey RequestContextKey = "content-path"
227292
)

gateway/gateway_test.go

+133-2
Original file line numberDiff line numberDiff line change
@@ -198,14 +198,20 @@ func newTestServerAndNode(t *testing.T, ns mockNamesys) (*httptest.Server, *mock
198198
}
199199

200200
func newTestServer(t *testing.T, api IPFSBackend) *httptest.Server {
201-
config := Config{Headers: map[string][]string{}}
201+
return newTestServerWithConfig(t, api, Config{
202+
Headers: map[string][]string{},
203+
TrustedMode: true,
204+
})
205+
}
206+
207+
func newTestServerWithConfig(t *testing.T, api IPFSBackend, config Config) *httptest.Server {
202208
AddAccessControlHeaders(config.Headers)
203209

204210
handler := NewHandler(config, api)
205211
mux := http.NewServeMux()
206212
mux.Handle("/ipfs/", handler)
207213
mux.Handle("/ipns/", handler)
208-
handler = WithHostname(mux, api, map[string]*Specification{}, false)
214+
handler = WithHostname(config, api, mux)
209215

210216
ts := httptest.NewServer(handler)
211217
t.Cleanup(func() { ts.Close() })
@@ -546,3 +552,128 @@ func TestGoGetSupport(t *testing.T) {
546552
assert.Nil(t, err)
547553
assert.Equal(t, http.StatusOK, res.StatusCode)
548554
}
555+
556+
func TestIpfsTrustlessMode(t *testing.T) {
557+
api, root := newMockAPI(t)
558+
559+
ts := newTestServerWithConfig(t, api, Config{
560+
Headers: map[string][]string{},
561+
NoDNSLink: false,
562+
PublicGateways: map[string]*Specification{
563+
"trustless.com": {
564+
Paths: []string{"/ipfs", "/ipns"},
565+
},
566+
"trusted.com": {
567+
Paths: []string{"/ipfs", "/ipns"},
568+
TrustedMode: true,
569+
},
570+
},
571+
})
572+
t.Logf("test server url: %s", ts.URL)
573+
574+
trustedFormats := []string{"", "dag-json", "dag-cbor", "tar", "json", "cbor"}
575+
trustlessFormats := []string{"raw", "car"}
576+
577+
doRequest := func(t *testing.T, path, host string, expectedStatus int) {
578+
req, err := http.NewRequest(http.MethodGet, ts.URL+path, nil)
579+
assert.Nil(t, err)
580+
581+
if host != "" {
582+
req.Host = host
583+
}
584+
585+
res, err := doWithoutRedirect(req)
586+
assert.Nil(t, err)
587+
defer res.Body.Close()
588+
assert.Equal(t, expectedStatus, res.StatusCode)
589+
}
590+
591+
doIpfsCidRequests := func(t *testing.T, formats []string, host string, expectedStatus int) {
592+
for _, format := range formats {
593+
doRequest(t, "/ipfs/"+root.String()+"/?format="+format, host, expectedStatus)
594+
}
595+
}
596+
597+
doIpfsCidPathRequests := func(t *testing.T, formats []string, host string, expectedStatus int) {
598+
for _, format := range formats {
599+
doRequest(t, "/ipfs/"+root.String()+"/EmptyDir/?format="+format, host, expectedStatus)
600+
}
601+
}
602+
603+
trustedTests := func(t *testing.T, host string) {
604+
doIpfsCidRequests(t, trustlessFormats, host, http.StatusOK)
605+
doIpfsCidRequests(t, trustedFormats, host, http.StatusOK)
606+
doIpfsCidPathRequests(t, trustlessFormats, host, http.StatusOK)
607+
doIpfsCidPathRequests(t, trustedFormats, host, http.StatusOK)
608+
}
609+
610+
trustlessTests := func(t *testing.T, host string) {
611+
doIpfsCidRequests(t, trustlessFormats, host, http.StatusOK)
612+
doIpfsCidRequests(t, trustedFormats, host, http.StatusNotImplemented)
613+
doIpfsCidPathRequests(t, trustlessFormats, host, http.StatusNotImplemented)
614+
doIpfsCidPathRequests(t, trustedFormats, host, http.StatusNotImplemented)
615+
}
616+
617+
t.Run("Explicit Trustless Gateway", func(t *testing.T) {
618+
t.Parallel()
619+
trustlessTests(t, "trustless.com")
620+
})
621+
622+
t.Run("Explicit Trusted Gateway", func(t *testing.T) {
623+
t.Parallel()
624+
trustedTests(t, "trusted.com")
625+
})
626+
627+
t.Run("Implicit Default Trustless Gateway", func(t *testing.T) {
628+
t.Parallel()
629+
trustlessTests(t, "not.configured.com")
630+
trustlessTests(t, "localhost")
631+
trustlessTests(t, "127.0.0.1")
632+
trustlessTests(t, "::1")
633+
})
634+
}
635+
636+
func TestIpnsTrustlessMode(t *testing.T) {
637+
api, root := newMockAPI(t)
638+
api.namesys["/ipns/trustless.com"] = path.FromCid(root)
639+
api.namesys["/ipns/trusted.com"] = path.FromCid(root)
640+
641+
ts := newTestServerWithConfig(t, api, Config{
642+
Headers: map[string][]string{},
643+
NoDNSLink: false,
644+
PublicGateways: map[string]*Specification{
645+
"trustless.com": {
646+
Paths: []string{"/ipfs", "/ipns"},
647+
},
648+
"trusted.com": {
649+
Paths: []string{"/ipfs", "/ipns"},
650+
TrustedMode: true,
651+
},
652+
},
653+
})
654+
t.Logf("test server url: %s", ts.URL)
655+
656+
doRequest := func(t *testing.T, path, host string, expectedStatus int) {
657+
req, err := http.NewRequest(http.MethodGet, ts.URL+path, nil)
658+
assert.Nil(t, err)
659+
660+
if host != "" {
661+
req.Host = host
662+
}
663+
664+
res, err := doWithoutRedirect(req)
665+
assert.Nil(t, err)
666+
defer res.Body.Close()
667+
assert.Equal(t, expectedStatus, res.StatusCode)
668+
}
669+
670+
// DNSLink only. Not supported for trustless. Supported for trusted, except
671+
// format=ipns-record which is unavailable for DNSLink.
672+
doRequest(t, "/", "trustless.com", http.StatusNotImplemented)
673+
doRequest(t, "/EmptyDir/", "trustless.com", http.StatusNotImplemented)
674+
doRequest(t, "/?format=ipns-record", "trustless.com", http.StatusNotImplemented)
675+
676+
doRequest(t, "/", "trusted.com", http.StatusOK)
677+
doRequest(t, "/EmptyDir/", "trusted.com", http.StatusOK)
678+
doRequest(t, "/?format=ipns-record", "trusted.com", http.StatusBadRequest)
679+
}

gateway/handler.go

+64
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"time"
1818

1919
ipath "github.com/ipfs/boxo/coreiface/path"
20+
"github.com/ipfs/boxo/ipns"
2021
cid "github.com/ipfs/go-cid"
2122
logging "github.com/ipfs/go-log"
2223
prometheus "github.com/prometheus/client_golang/prometheus"
@@ -232,6 +233,13 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) {
232233
i.addUserHeaders(w) // ok, _now_ write user's headers.
233234
w.Header().Set("X-Ipfs-Path", contentPath.String())
234235

236+
// Trustless gateway.
237+
if !i.isTrustedMode(r) && !i.isValidTrustlessRequest(contentPath, responseFormat) {
238+
err := errors.New("only trustless requests are accepted: https://specs.ipfs.tech/http-gateways/trustless-gateway/")
239+
webError(w, err, http.StatusNotImplemented)
240+
return
241+
}
242+
235243
// TODO: Why did the previous code do path resolution, was that a bug?
236244
// TODO: Does If-None-Match apply here?
237245
if responseFormat == "application/vnd.ipfs.ipns-record" {
@@ -315,6 +323,62 @@ func (i *handler) addUserHeaders(w http.ResponseWriter) {
315323
}
316324
}
317325

326+
func (i *handler) isTrustedMode(r *http.Request) bool {
327+
// Get the host, by default the request's Host. If this request went through
328+
// WithHostname, also check for the key in the context. If that is not present,
329+
// also check X-Forwarded-Host to support reverse proxies.
330+
host := r.Host
331+
if h, ok := r.Context().Value(GatewayHostnameKey).(string); ok {
332+
host = h
333+
} else if xHost := r.Header.Get("X-Forwarded-Host"); xHost != "" {
334+
host = xHost
335+
}
336+
337+
// If the gateway is defined, return whatever is set.
338+
if gw, ok := i.config.PublicGateways[host]; ok {
339+
return gw.TrustedMode
340+
}
341+
342+
// Otherwise, the default.
343+
return i.config.TrustedMode
344+
}
345+
346+
func (i *handler) isValidTrustlessRequest(contentPath ipath.Path, responseFormat string) bool {
347+
// Only allow "/{#1}/{#2}"-like paths.
348+
trimmedPath := strings.Trim(contentPath.String(), "/")
349+
pathComponents := strings.Split(trimmedPath, "/")
350+
if len(pathComponents) != 2 {
351+
return false
352+
}
353+
354+
if contentPath.Namespace() == "ipns" {
355+
// Only ipns records allowed until https://github.com/ipfs/specs/issues/369 is resolved
356+
if responseFormat != "application/vnd.ipfs.ipns-record" {
357+
return false
358+
}
359+
360+
// Only valid IPNS names, no DNSLink.
361+
if _, err := ipns.Decode(pathComponents[1]); err != nil {
362+
return false
363+
}
364+
365+
return true
366+
}
367+
368+
// Only valid CIDs.
369+
if _, err := cid.Decode(pathComponents[1]); err != nil {
370+
return false
371+
}
372+
373+
switch responseFormat {
374+
case "application/vnd.ipld.raw",
375+
"application/vnd.ipld.car":
376+
return true
377+
default:
378+
return false
379+
}
380+
}
381+
318382
func panicHandler(w http.ResponseWriter) {
319383
if r := recover(); r != nil {
320384
log.Error("A panic occurred in the gateway handler!")

0 commit comments

Comments
 (0)