Skip to content

Commit 2264ac2

Browse files
committed
feat(gateway)!: trustless mode
1 parent 7c7aa8d commit 2264ac2

File tree

8 files changed

+248
-86
lines changed

8 files changed

+248
-86
lines changed

.github/workflows/gateway-sharness.yml

-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ jobs:
2828
with:
2929
repository: ipfs/kubo
3030
path: kubo
31-
ref: 503edee648e29c62888f05fa146ab13d9c65077d
3231
- name: Install Missing Tools
3332
run: sudo apt install -y socat net-tools fish libxml2-utils
3433
- name: Restore Go Cache

examples/gateway/common/handler.go

+33-24
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,43 @@ import (
99
)
1010

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

20-
// Initialize the public gateways that we will want to have available through
21-
// Host header rewriting. This step is optional and only required if you're
22-
// running multiple public gateways and want different settings and support
23-
// for DNSLink and Subdomain Gateways.
24-
noDNSLink := false // If you set DNSLink to point at the CID from CAR, you can load it!
25-
publicGateways := map[string]*gateway.Specification{
26-
// Support public requests with Host: CID.ipfs.example.net and ID.ipns.example.net
27-
"example.net": {
28-
Paths: []string{"/ipfs", "/ipns"},
29-
NoDNSLink: noDNSLink,
30-
UseSubdomains: true,
31-
},
32-
// Support local requests
33-
"localhost": {
34-
Paths: []string{"/ipfs", "/ipns"},
35-
NoDNSLink: noDNSLink,
36-
UseSubdomains: true,
17+
// If you set DNSLink to point at the CID from CAR, you can load it!
18+
NoDNSLink: false,
19+
20+
TrustedMode: true,
21+
22+
// Initialize the public gateways that we will want to have available through
23+
// Host header rewriting. This step is optional and only required if you're
24+
// running multiple public gateways and want different settings and support
25+
// for DNSLink and Subdomain Gateways.
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: false,
31+
UseSubdomains: true,
32+
// This gateway is used for testing and therefore we make non-trustless
33+
// requests. Thus, we have to manually turn on the trusted mode.
34+
TrustedMode: true,
35+
},
36+
// Support local requests
37+
"localhost": {
38+
Paths: []string{"/ipfs", "/ipns"},
39+
NoDNSLink: false,
40+
UseSubdomains: true,
41+
TrustedMode: true,
42+
},
3743
},
3844
}
3945

46+
// Add required access control headers to the configuration.
47+
gateway.AddAccessControlHeaders(conf.Headers)
48+
4049
// Creates a mux to serve the gateway paths. This is not strictly necessary
4150
// and gwHandler could be used directly. However, on the next step we also want
4251
// to add prometheus metrics, hence needing the mux.
@@ -56,7 +65,7 @@ func NewHandler(gwAPI gateway.IPFSBackend) http.Handler {
5665
// or example.net. If you want to expose the metrics on such gateways,
5766
// you will have to add the path "/debug" to the variable Paths.
5867
var handler http.Handler
59-
handler = gateway.WithHostname(mux, gwAPI, publicGateways, noDNSLink)
68+
handler = gateway.WithHostname(conf, gwAPI, mux)
6069

6170
// Finally, wrap with the withConnect middleware. This is required since we use
6271
// 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

+80-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() })
@@ -544,3 +550,75 @@ func TestGoGetSupport(t *testing.T) {
544550
assert.Nil(t, err)
545551
assert.Equal(t, http.StatusOK, res.StatusCode)
546552
}
553+
554+
func TestTrustlessMode(t *testing.T) {
555+
api, root := newMockAPI(t)
556+
ts := newTestServerWithConfig(t, api, Config{
557+
Headers: map[string][]string{},
558+
NoDNSLink: false,
559+
PublicGateways: map[string]*Specification{
560+
"trustless.com": {
561+
Paths: []string{"/ipfs", "/ipns"},
562+
},
563+
"trusted.com": {
564+
Paths: []string{"/ipfs", "/ipns"},
565+
TrustedMode: true,
566+
},
567+
},
568+
})
569+
t.Logf("test server url: %s", ts.URL)
570+
571+
trustedFormats := []string{"", "dag-json", "dag-cbor", "tar", "json", "cbor"}
572+
trustlessFormats := []string{"raw", "car"}
573+
574+
doRequests := func(formats []string, host string, expectedStatus int) {
575+
for _, format := range formats {
576+
req, err := http.NewRequest(http.MethodGet, ts.URL+"/ipfs/"+root.String()+"/?format="+format, nil)
577+
assert.Nil(t, err)
578+
579+
if host != "" {
580+
req.Host = host
581+
}
582+
583+
res, err := doWithoutRedirect(req)
584+
assert.Nil(t, err)
585+
defer res.Body.Close()
586+
assert.Equal(t, expectedStatus, res.StatusCode)
587+
}
588+
}
589+
590+
t.Run("Explicit Trustless Gateway", func(t *testing.T) {
591+
t.Parallel()
592+
doRequests(trustlessFormats, "trustless.com", http.StatusOK)
593+
doRequests(trustedFormats, "trustless.com", http.StatusNotImplemented)
594+
})
595+
596+
t.Run("Explicit Trusted Gateway", func(t *testing.T) {
597+
t.Parallel()
598+
doRequests(trustlessFormats, "trusted.com", http.StatusOK)
599+
doRequests(trustedFormats, "trusted.com", http.StatusOK)
600+
})
601+
602+
t.Run("Implicit Default Trustless Gateway", func(t *testing.T) {
603+
t.Parallel()
604+
doRequests(trustlessFormats, "not.configured.com", http.StatusOK)
605+
doRequests(trustedFormats, "not.configured.com", http.StatusNotImplemented)
606+
})
607+
608+
t.Run("Implicit Default Local Trusted Gateway", func(t *testing.T) {
609+
t.Parallel()
610+
doRequests(trustlessFormats, "localhost", http.StatusOK)
611+
doRequests(trustedFormats, "localhost", http.StatusOK)
612+
doRequests(trustlessFormats, "127.0.0.1", http.StatusOK)
613+
doRequests(trustedFormats, "127.0.0.1", http.StatusOK)
614+
doRequests(trustlessFormats, "::1", http.StatusOK)
615+
doRequests(trustedFormats, "::1", http.StatusOK)
616+
617+
doRequests(trustlessFormats, "localhost:8080", http.StatusOK)
618+
doRequests(trustedFormats, "localhost:8080", http.StatusOK)
619+
doRequests(trustlessFormats, "127.0.0.1:8080", http.StatusOK)
620+
doRequests(trustedFormats, "127.0.0.1:8080", http.StatusOK)
621+
doRequests(trustlessFormats, "[::1]:8080", http.StatusOK)
622+
doRequests(trustedFormats, "[::1]:8080", http.StatusOK)
623+
})
624+
}

gateway/handler.go

+39
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,19 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) {
277277
}
278278
}
279279

280+
if !i.isTrustedMode(r) {
281+
switch responseFormat {
282+
case "application/vnd.ipld.raw",
283+
"application/vnd.ipld.car",
284+
"application/vnd.ipfs.ipns-record":
285+
// Allowed
286+
default:
287+
err := errors.New("only trustless requests are accepted: https://specs.ipfs.tech/http-gateways/trustless-gateway/")
288+
webError(w, err, http.StatusNotImplemented)
289+
return
290+
}
291+
}
292+
280293
var success bool
281294

282295
// Support custom response formats passed via ?format or Accept HTTP header
@@ -314,6 +327,32 @@ func (i *handler) addUserHeaders(w http.ResponseWriter) {
314327
}
315328
}
316329

330+
func (i *handler) isTrustedMode(r *http.Request) bool {
331+
// Get the host, by default the request's Host. If this request went through
332+
// WithHostname, also check for the key in the context. If that is not present,
333+
// also check X-Forwarded-Host to support reverse proxies.
334+
host := r.Host
335+
if h, ok := r.Context().Value(GatewayHostnameKey).(string); ok {
336+
host = h
337+
} else if xHost := r.Header.Get("X-Forwarded-Host"); xHost != "" {
338+
host = xHost
339+
}
340+
341+
// If the gateway is defined, return whatever is set.
342+
if gw, ok := i.config.PublicGateways[host]; ok {
343+
return gw.TrustedMode
344+
}
345+
346+
// Cleanup the host and check if it's a local gateway. If so, it's trusted.
347+
host = stripPort(host)
348+
if host == "127.0.0.1" || host == "::1" || host == "localhost" {
349+
return true
350+
}
351+
352+
// Otherwise, the default.
353+
return i.config.TrustedMode
354+
}
355+
317356
func panicHandler(w http.ResponseWriter) {
318357
if r := recover(); r != nil {
319358
log.Error("A panic occurred in the gateway handler!")

gateway/handler_unixfs__redirects.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,10 @@ func (i *handler) serve4xx(w http.ResponseWriter, r *http.Request, content4xxPat
210210
}
211211

212212
func hasOriginIsolation(r *http.Request) bool {
213-
_, gw := r.Context().Value(GatewayHostnameKey).(string)
213+
_, subdomainGw := r.Context().Value(SubdomainHostnameKey).(string)
214214
_, dnslink := r.Context().Value(DNSLinkHostnameKey).(string)
215215

216-
if gw || dnslink {
216+
if subdomainGw || dnslink {
217217
return true
218218
}
219219

gateway/handler_unixfs_dir.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,10 @@ func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r *
186186
// for this request.
187187
var gwURL string
188188

189-
// Get gateway hostname and build gateway URL.
190-
if h, ok := r.Context().Value(GatewayHostnameKey).(string); ok {
189+
// Ensure correct URL in DNSLink and Subdomain Gateways.
190+
if h, ok := r.Context().Value(SubdomainHostnameKey).(string); ok {
191+
gwURL = "//" + h
192+
} else if h, ok := r.Context().Value(DNSLinkHostnameKey).(string); ok {
191193
gwURL = "//" + h
192194
} else {
193195
gwURL = ""

0 commit comments

Comments
 (0)