feat(gateway): subdomain and proxy gateway
License: MIT Signed-off-by: Marcin Rataj <lidel@lidel.org>
Marcin Rataj committed
Mar 14, 2019 at 17:21 UTC
3ecccd6e1dff567db8da8a53cebd8226ecf2f446
18 files changed
+1421
-85
cmd/ipfs/daemon.go
+6
-5
@@ -12,6 +12,8 @@ import (
12
"sort"
13
"sync"
14
15
+ multierror "github.com/hashicorp/go-multierror"
16
+
17
version "github.com/ipfs/go-ipfs"
18
config "github.com/ipfs/go-ipfs-config"
19
cserial "github.com/ipfs/go-ipfs-config/serialize"
@@ -27,7 +29,6 @@ import (
29
migrate "github.com/ipfs/go-ipfs/repo/fsrepo/migrations"
30
sockets "github.com/libp2p/go-socket-activation"
31
30
- "github.com/hashicorp/go-multierror"
32
cmds "github.com/ipfs/go-ipfs-cmds"
33
mprome "github.com/ipfs/go-metrics-prometheus"
34
goprocess "github.com/jbenet/goprocess"
@@ -298,9 +299,9 @@ func daemonFunc(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment
299
300
// Start assembling node config
301
ncfg := &core.BuildCfg{
301
- Repo: repo,
302
- Permanent: true, // It is temporary way to signify that node is permanent
303
- Online: !offline,
302
+ Repo: repo,
303
+ Permanent: true, // It is temporary way to signify that node is permanent
304
+ Online: !offline,
305
DisableEncryptedConnections: unencrypted,
306
ExtraOpts: map[string]bool{
307
"pubsub": pubsub,
@@ -636,7 +637,7 @@ func serveHTTPGateway(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, e
637
638
var opts = []corehttp.ServeOption{
639
corehttp.MetricsCollectionOption("gateway"),
639
- corehttp.IPNSHostnameOption(),
640
+ corehttp.HostnameOption(),
641
corehttp.GatewayOption(writable, "/ipfs", "/ipns"),
642
corehttp.VersionOption(),
643
corehttp.CheckVersionOption(),
core/corehttp/corehttp.go
+13
-1
@@ -43,7 +43,17 @@ func makeHandler(n *core.IpfsNode, l net.Listener, options ...ServeOption) (http
43
return nil, err
44
}
45
}
46
- return topMux, nil
46
+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
47
+ // ServeMux does not support requests with CONNECT method,
48
+ // so we need to handle them separately
49
+ // https://golang.org/src/net/http/request.go#L111
50
+ if r.Method == http.MethodConnect {
51
+ w.WriteHeader(http.StatusOK)
52
+ return
53
+ }
54
+ topMux.ServeHTTP(w, r)
55
+ })
56
+ return handler, nil
57
}
58
59
// ListenAndServe runs an HTTP server listening at |listeningMultiAddr| with
@@ -70,6 +80,8 @@ func ListenAndServe(n *core.IpfsNode, listeningMultiAddr string, options ...Serv
80
return Serve(n, manet.NetListener(list), options...)
81
}
82
83
+// Serve accepts incoming HTTP connections on the listener and pass them
84
+// to ServeOption handlers.
85
func Serve(node *core.IpfsNode, lis net.Listener, options ...ServeOption) error {
86
// make sure we close this no matter what.
87
defer lis.Close()
core/corehttp/gateway_handler.go
+5
-4
@@ -14,12 +14,12 @@ import (
14
"strings"
15
"time"
16
17
- "github.com/dustin/go-humanize"
17
+ humanize "github.com/dustin/go-humanize"
18
"github.com/ipfs/go-cid"
19
files "github.com/ipfs/go-ipfs-files"
20
dag "github.com/ipfs/go-merkledag"
21
- "github.com/ipfs/go-mfs"
22
- "github.com/ipfs/go-path"
21
+ mfs "github.com/ipfs/go-mfs"
22
+ path "github.com/ipfs/go-path"
23
"github.com/ipfs/go-path/resolver"
24
coreiface "github.com/ipfs/interface-go-ipfs-core"
25
ipath "github.com/ipfs/interface-go-ipfs-core/path"
@@ -142,7 +142,7 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
142
}
143
}
144
145
- // IPNSHostnameOption might have constructed an IPNS path using the Host header.
145
+ // HostnameOption might have constructed an IPNS/IPFS path using the Host header.
146
// In this case, we need the original path for constructing redirects
147
// and links that match the requested URL.
148
// For example, http://example.net would become /ipns/example.net, and
@@ -150,6 +150,7 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
150
requestURI, err := url.ParseRequestURI(r.RequestURI)
151
if err != nil {
152
webError(w, "failed to parse request path", err, http.StatusInternalServerError)
153
+ return
154
}
155
originalUrlPath := prefix + requestURI.Path
156
core/corehttp/gateway_test.go
+9
-9
@@ -138,7 +138,7 @@ func newTestServerAndNode(t *testing.T, ns mockNamesys) (*httptest.Server, iface
138
139
dh.Handler, err = makeHandler(n,
140
ts.Listener,
141
- IPNSHostnameOption(),
141
+ HostnameOption(),
142
GatewayOption(false, "/ipfs", "/ipns"),
143
VersionOption(),
144
)
@@ -184,12 +184,12 @@ func TestGatewayGet(t *testing.T) {
184
status int
185
text string
186
}{
187
- {"localhost:5001", "/", http.StatusNotFound, "404 page not found\n"},
188
- {"localhost:5001", "/" + k.Cid().String(), http.StatusNotFound, "404 page not found\n"},
189
- {"localhost:5001", k.String(), http.StatusOK, "fnord"},
190
- {"localhost:5001", "/ipns/nxdomain.example.com", http.StatusNotFound, "ipfs resolve -r /ipns/nxdomain.example.com: " + namesys.ErrResolveFailed.Error() + "\n"},
191
- {"localhost:5001", "/ipns/%0D%0A%0D%0Ahello", http.StatusNotFound, "ipfs resolve -r /ipns/%0D%0A%0D%0Ahello: " + namesys.ErrResolveFailed.Error() + "\n"},
192
- {"localhost:5001", "/ipns/example.com", http.StatusOK, "fnord"},
187
+ {"127.0.0.1:8080", "/", http.StatusNotFound, "404 page not found\n"},
188
+ {"127.0.0.1:8080", "/" + k.Cid().String(), http.StatusNotFound, "404 page not found\n"},
189
+ {"127.0.0.1:8080", k.String(), http.StatusOK, "fnord"},
190
+ {"127.0.0.1:8080", "/ipns/nxdomain.example.com", http.StatusNotFound, "ipfs resolve -r /ipns/nxdomain.example.com: " + namesys.ErrResolveFailed.Error() + "\n"},
191
+ {"127.0.0.1:8080", "/ipns/%0D%0A%0D%0Ahello", http.StatusNotFound, "ipfs resolve -r /ipns/%0D%0A%0D%0Ahello: " + namesys.ErrResolveFailed.Error() + "\n"},
192
+ {"127.0.0.1:8080", "/ipns/example.com", http.StatusOK, "fnord"},
193
{"example.com", "/", http.StatusOK, "fnord"},
194
195
{"working.example.com", "/", http.StatusOK, "fnord"},
@@ -381,7 +381,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
381
if !strings.Contains(s, "Index of /foo? #<'/") {
382
t.Fatalf("expected a path in directory listing")
383
}
384
- if !strings.Contains(s, "<a href=\"/\">") {
384
+ if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/./..\">") {
385
t.Fatalf("expected backlink in directory listing")
386
}
387
if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/file.txt\">") {
@@ -447,7 +447,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
447
if !strings.Contains(s, "Index of /foo? #<'/bar/") {
448
t.Fatalf("expected a path in directory listing")
449
}
450
- if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/\">") {
450
+ if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/bar/./..\">") {
451
t.Fatalf("expected backlink in directory listing")
452
}
453
if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/bar/file.txt\">") {
core/corehttp/hostname.go
new
+358
@@ -0,0 +1,358 @@
1
+package corehttp
2
+
3
+import (
4
+ "context"
5
+ "fmt"
6
+ "net"
7
+ "net/http"
8
+ "net/url"
9
+ "strings"
10
+
11
+ cid "github.com/ipfs/go-cid"
12
+ core "github.com/ipfs/go-ipfs/core"
13
+ coreapi "github.com/ipfs/go-ipfs/core/coreapi"
14
+ namesys "github.com/ipfs/go-ipfs/namesys"
15
+ isd "github.com/jbenet/go-is-domain"
16
+ "github.com/libp2p/go-libp2p-core/peer"
17
+ mbase "github.com/multiformats/go-multibase"
18
+
19
+ config "github.com/ipfs/go-ipfs-config"
20
+ iface "github.com/ipfs/interface-go-ipfs-core"
21
+ options "github.com/ipfs/interface-go-ipfs-core/options"
22
+ nsopts "github.com/ipfs/interface-go-ipfs-core/options/namesys"
23
+)
24
+
25
+var defaultPaths = []string{"/ipfs/", "/ipns/", "/api/", "/p2p/", "/version"}
26
+
27
+var pathGatewaySpec = config.GatewaySpec{
28
+ Paths: defaultPaths,
29
+ UseSubdomains: false,
30
+}
31
+
32
+var subdomainGatewaySpec = config.GatewaySpec{
33
+ Paths: defaultPaths,
34
+ UseSubdomains: true,
35
+}
36
+
37
+var defaultKnownGateways = map[string]config.GatewaySpec{
38
+ "localhost": subdomainGatewaySpec,
39
+ "ipfs.io": pathGatewaySpec,
40
+ "gateway.ipfs.io": pathGatewaySpec,
41
+ "dweb.link": subdomainGatewaySpec,
42
+}
43
+
44
+// HostnameOption rewrites an incoming request based on the Host header.
45
+func HostnameOption() ServeOption {
46
+ return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
47
+ childMux := http.NewServeMux()
48
+
49
+ coreApi, err := coreapi.NewCoreAPI(n)
50
+ if err != nil {
51
+ return nil, err
52
+ }
53
+
54
+ cfg, err := n.Repo.Config()
55
+ if err != nil {
56
+ return nil, err
57
+ }
58
+ knownGateways := make(
59
+ map[string]config.GatewaySpec,
60
+ len(defaultKnownGateways)+len(cfg.Gateway.PublicGateways),
61
+ )
62
+ for hostname, gw := range defaultKnownGateways {
63
+ knownGateways[hostname] = gw
64
+ }
65
+ for hostname, gw := range cfg.Gateway.PublicGateways {
66
+ if gw == nil {
67
+ // Allows the user to remove gateways but _also_
68
+ // allows us to continuously update the list.
69
+ delete(knownGateways, hostname)
70
+ } else {
71
+ knownGateways[hostname] = *gw
72
+ }
73
+ }
74
+
75
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
76
+ // Unfortunately, many (well, ipfs.io) gateways use
77
+ // DNSLink so if we blindly rewrite with DNSLink, we'll
78
+ // break /ipfs links.
79
+ //
80
+ // We fix this by maintaining a list of known gateways
81
+ // and the paths that they serve "gateway" content on.
82
+ // That way, we can use DNSLink for everything else.
83
+
84
+ // HTTP Host & Path check: is this one of our "known gateways"?
85
+ if gw, ok := isKnownHostname(r.Host, knownGateways); ok {
86
+ // This is a known gateway but request is not using
87
+ // the subdomain feature.
88
+
89
+ // Does this gateway _handle_ this path?
90
+ if hasPrefix(r.URL.Path, gw.Paths...) {
91
+ // It does.
92
+
93
+ // Should this gateway use subdomains instead of paths?
94
+ if gw.UseSubdomains {
95
+ // Yes, redirect if applicable
96
+ // Example: dweb.link/ipfs/{cid} → {cid}.ipfs.dweb.link
97
+ if newURL, ok := toSubdomainURL(r.Host, r.URL.Path, r); ok {
98
+ http.Redirect(w, r, newURL, http.StatusMovedPermanently)
99
+ return
100
+ }
101
+ }
102
+
103
+ // Not a subdomain resource, continue with path processing
104
+ // Example: 127.0.0.1:8080/ipfs/{CID}, ipfs.io/ipfs/{CID} etc
105
+ childMux.ServeHTTP(w, r)
106
+ return
107
+ }
108
+ // Not a whitelisted path
109
+
110
+ // Try DNSLink, if it was not explicitly disabled for the hostname
111
+ if !gw.NoDNSLink && isDNSLinkRequest(n.Context(), coreApi, r) {
112
+ // rewrite path and handle as DNSLink
113
+ r.URL.Path = "/ipns/" + stripPort(r.Host) + r.URL.Path
114
+ childMux.ServeHTTP(w, r)
115
+ return
116
+ }
117
+
118
+ // If not, resource does not exist on the hostname, return 404
119
+ http.NotFound(w, r)
120
+ return
121
+ }
122
+
123
+ // HTTP Host check: is this one of our subdomain-based "known gateways"?
124
+ // Example: {cid}.ipfs.localhost, {cid}.ipfs.dweb.link
125
+ if gw, hostname, ns, rootID, ok := knownSubdomainDetails(r.Host, knownGateways); ok {
126
+ // Looks like we're using known subdomain gateway.
127
+
128
+ // Assemble original path prefix.
129
+ pathPrefix := "/" + ns + "/" + rootID
130
+
131
+ // Does this gateway _handle_ this path?
132
+ if !(gw.UseSubdomains && hasPrefix(pathPrefix, gw.Paths...)) {
133
+ // If not, resource does not exist, return 404
134
+ http.NotFound(w, r)
135
+ return
136
+ }
137
+
138
+ // Do we need to fix multicodec in PeerID represented as CIDv1?
139
+ if isPeerIDNamespace(ns) {
140
+ keyCid, err := cid.Decode(rootID)
141
+ if err == nil && keyCid.Type() != cid.Libp2pKey {
142
+ if newURL, ok := toSubdomainURL(hostname, pathPrefix+r.URL.Path, r); ok {
143
+ // Redirect to CID fixed inside of toSubdomainURL()
144
+ http.Redirect(w, r, newURL, http.StatusMovedPermanently)
145
+ return
146
+ }
147
+ }
148
+ }
149
+
150
+ // Rewrite the path to not use subdomains
151
+ r.URL.Path = pathPrefix + r.URL.Path
152
+
153
+ // Serve path request
154
+ childMux.ServeHTTP(w, r)
155
+ return
156
+ }
157
+ // We don't have a known gateway. Fallback on DNSLink lookup
158
+
159
+ // Wildcard HTTP Host check:
160
+ // 1. is wildcard DNSLink enabled (Gateway.NoDNSLink=false)?
161
+ // 2. does Host header include a fully qualified domain name (FQDN)?
162
+ // 3. does DNSLink record exist in DNS?
163
+ if !cfg.Gateway.NoDNSLink && isDNSLinkRequest(n.Context(), coreApi, r) {
164
+ // rewrite path and handle as DNSLink
165
+ r.URL.Path = "/ipns/" + stripPort(r.Host) + r.URL.Path
166
+ childMux.ServeHTTP(w, r)
167
+ return
168
+ }
169
+
170
+ // else, treat it as an old school gateway, I guess.
171
+ childMux.ServeHTTP(w, r)
172
+ })
173
+ return childMux, nil
174
+ }
175
+}
176
+
177
+// isKnownHostname checks Gateway.PublicGateways and returns matching
178
+// GatewaySpec with gracefull fallback to version without port
179
+func isKnownHostname(hostname string, knownGateways map[string]config.GatewaySpec) (gw config.GatewaySpec, ok bool) {
180
+ // Try hostname (host+optional port - value from Host header as-is)
181
+ if gw, ok := knownGateways[hostname]; ok {
182
+ return gw, ok
183
+ }
184
+ // Fallback to hostname without port
185
+ gw, ok = knownGateways[stripPort(hostname)]
186
+ return gw, ok
187
+}
188
+
189
+// Parses Host header and looks for a known subdomain gateway host.
190
+// If found, returns GatewaySpec and subdomain components.
191
+// Note: hostname is host + optional port
192
+func knownSubdomainDetails(hostname string, knownGateways map[string]config.GatewaySpec) (gw config.GatewaySpec, knownHostname, ns, rootID string, ok bool) {
193
+ labels := strings.Split(hostname, ".")
194
+ // Look for FQDN of a known gateway hostname.
195
+ // Example: given "dist.ipfs.io.ipns.dweb.link":
196
+ // 1. Lookup "link" TLD in knownGateways: negative
197
+ // 2. Lookup "dweb.link" in knownGateways: positive
198
+ //
199
+ // Stops when we have 2 or fewer labels left as we need at least a
200
+ // rootId and a namespace.
201
+ for i := len(labels) - 1; i >= 2; i-- {
202
+ fqdn := strings.Join(labels[i:], ".")
203
+ gw, ok := isKnownHostname(fqdn, knownGateways)
204
+ if !ok {
205
+ continue
206
+ }
207
+
208
+ ns := labels[i-1]
209
+ if !isSubdomainNamespace(ns) {
210
+ break
211
+ }
212
+
213
+ // Merge remaining labels (could be a FQDN with DNSLink)
214
+ rootID := strings.Join(labels[:i-1], ".")
215
+ return gw, fqdn, ns, rootID, true
216
+ }
217
+ // not a known subdomain gateway
218
+ return gw, "", "", "", false
219
+}
220
+
221
+// isDNSLinkRequest returns bool that indicates if request
222
+// should return data from content path listed in DNSLink record (if exists)
223
+func isDNSLinkRequest(ctx context.Context, ipfs iface.CoreAPI, r *http.Request) bool {
224
+ fqdn := stripPort(r.Host)
225
+ if len(fqdn) == 0 && !isd.IsDomain(fqdn) {
226
+ return false
227
+ }
228
+ name := "/ipns/" + fqdn
229
+ // check if DNSLink exists
230
+ depth := options.Name.ResolveOption(nsopts.Depth(1))
231
+ _, err := ipfs.Name().Resolve(ctx, name, depth)
232
+ return err == nil || err == namesys.ErrResolveRecursion
233
+}
234
+
235
+func isSubdomainNamespace(ns string) bool {
236
+ switch ns {
237
+ case "ipfs", "ipns", "p2p", "ipld":
238
+ return true
239
+ default:
240
+ return false
241
+ }
242
+}
243
+
244
+func isPeerIDNamespace(ns string) bool {
245
+ switch ns {
246
+ case "ipns", "p2p":
247
+ return true
248
+ default:
249
+ return false
250
+ }
251
+}
252
+
253
+// Converts a hostname/path to a subdomain-based URL, if applicable.
254
+func toSubdomainURL(hostname, path string, r *http.Request) (redirURL string, ok bool) {
255
+ var scheme, ns, rootID, rest string
256
+
257
+ query := r.URL.RawQuery
258
+ parts := strings.SplitN(path, "/", 4)
259
+ safeRedirectURL := func(in string) (out string, ok bool) {
260
+ safeURI, err := url.ParseRequestURI(in)
261
+ if err != nil {
262
+ return "", false
263
+ }
264
+ return safeURI.String(), true
265
+ }
266
+
267
+ // Support X-Forwarded-Proto if added by a reverse proxy
268
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
269
+ xproto := r.Header.Get("X-Forwarded-Proto")
270
+ if xproto == "https" {
271
+ scheme = "https:"
272
+ } else {
273
+ scheme = "http:"
274
+ }
275
+
276
+ switch len(parts) {
277
+ case 4:
278
+ rest = parts[3]
279
+ fallthrough
280
+ case 3:
281
+ ns = parts[1]
282
+ rootID = parts[2]
283
+ default:
284
+ return "", false
285
+ }
286
+
287
+ if !isSubdomainNamespace(ns) {
288
+ return "", false
289
+ }
290
+
291
+ // add prefix if query is present
292
+ if query != "" {
293
+ query = "?" + query
294
+ }
295
+
296
+ // Normalize problematic PeerIDs (eg. ed25519+identity) to CID representation
297
+ if isPeerIDNamespace(ns) && !isd.IsDomain(rootID) {
298
+ peerID, err := peer.Decode(rootID)
299
+ // Note: PeerID CIDv1 with protobuf multicodec will fail, but we fix it
300
+ // in the next block
301
+ if err == nil {
302
+ rootID = peer.ToCid(peerID).String()
303
+ }
304
+ }
305
+
306
+ // If rootID is a CID, ensure it uses DNS-friendly text representation
307
+ if rootCid, err := cid.Decode(rootID); err == nil {
308
+ multicodec := rootCid.Type()
309
+
310
+ // PeerIDs represented as CIDv1 are expected to have libp2p-key
311
+ // multicodec (https://github.com/libp2p/specs/pull/209).
312
+ // We ease the transition by fixing multicodec on the fly:
313
+ // https://github.com/ipfs/go-ipfs/issues/5287#issuecomment-492163929
314
+ if isPeerIDNamespace(ns) && multicodec != cid.Libp2pKey {
315
+ multicodec = cid.Libp2pKey
316
+ }
317
+
318
+ // if object turns out to be a valid CID,
319
+ // ensure text representation used in subdomain is CIDv1 in Base32
320
+ // https://github.com/ipfs/in-web-browsers/issues/89
321
+ rootID, err = cid.NewCidV1(multicodec, rootCid.Hash()).StringOfBase(mbase.Base32)
322
+ if err != nil {
323
+ // should not error, but if it does, its clealy not possible to
324
+ // produce a subdomain URL
325
+ return "", false
326
+ }
327
+ }
328
+
329
+ return safeRedirectURL(fmt.Sprintf(
330
+ "%s//%s.%s.%s/%s%s",
331
+ scheme,
332
+ rootID,
333
+ ns,
334
+ hostname,
335
+ rest,
336
+ query,
337
+ ))
338
+}
339
+
340
+func hasPrefix(path string, prefixes ...string) bool {
341
+ for _, prefix := range prefixes {
342
+ // Assume people are creative with trailing slashes in Gateway config
343
+ p := strings.TrimSuffix(prefix, "/")
344
+ // Support for both /version and /ipfs/$cid
345
+ if p == path || strings.HasPrefix(path, p+"/") {
346
+ return true
347
+ }
348
+ }
349
+ return false
350
+}
351
+
352
+func stripPort(hostname string) string {
353
+ host, _, err := net.SplitHostPort(hostname)
354
+ if err == nil {
355
+ return host
356
+ }
357
+ return hostname
358
+}
core/corehttp/hostname_test.go
new
+152
@@ -0,0 +1,152 @@
1
+package corehttp
2
+
3
+import (
4
+ "net/http/httptest"
5
+ "testing"
6
+
7
+ config "github.com/ipfs/go-ipfs-config"
8
+)
9
+
10
+func TestToSubdomainURL(t *testing.T) {
11
+ r := httptest.NewRequest("GET", "http://request-stub.example.com", nil)
12
+ for _, test := range []struct {
13
+ // in:
14
+ hostname string
15
+ path string
16
+ // out:
17
+ url string
18
+ ok bool
19
+ }{
20
+ // DNSLink
21
+ {"localhost", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", true},
22
+ // Hostname with port
23
+ {"localhost:8080", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", true},
24
+ // CIDv0 → CIDv1base32
25
+ {"localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", true},
26
+ // PeerID as CIDv1 needs to have libp2p-key multicodec
27
+ {"localhost", "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://bafzbeieqhtl2l3mrszjnhv6hf2iloiitsx7mexiolcnywnbcrzkqxwslja.ipns.localhost/", true},
28
+ {"localhost", "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://bafzbeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm.ipns.localhost/", true},
29
+ // PeerID: ed25519+identity multihash
30
+ {"localhost", "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://bafzaajaiaejcat4yhiwnr2qz73mtu6vrnj2krxlpfoa3wo2pllfi37quorgwh2jw.ipns.localhost/", true},
31
+ } {
32
+ url, ok := toSubdomainURL(test.hostname, test.path, r)
33
+ if ok != test.ok || url != test.url {
34
+ t.Errorf("(%s, %s) returned (%s, %t), expected (%s, %t)", test.hostname, test.path, url, ok, test.url, ok)
35
+ }
36
+ }
37
+}
38
+
39
+func TestHasPrefix(t *testing.T) {
40
+ for _, test := range []struct {
41
+ prefixes []string
42
+ path string
43
+ out bool
44
+ }{
45
+ {[]string{"/ipfs"}, "/ipfs/cid", true},
46
+ {[]string{"/ipfs/"}, "/ipfs/cid", true},
47
+ {[]string{"/version/"}, "/version", true},
48
+ {[]string{"/version"}, "/version", true},
49
+ } {
50
+ out := hasPrefix(test.path, test.prefixes...)
51
+ if out != test.out {
52
+ t.Errorf("(%+v, %s) returned '%t', expected '%t'", test.prefixes, test.path, out, test.out)
53
+ }
54
+ }
55
+}
56
+
57
+func TestPortStripping(t *testing.T) {
58
+ for _, test := range []struct {
59
+ in string
60
+ out string
61
+ }{
62
+ {"localhost:8080", "localhost"},
63
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.localhost:8080", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.localhost"},
64
+ {"example.com:443", "example.com"},
65
+ {"example.com", "example.com"},
66
+ {"foo-dweb.ipfs.pvt.k12.ma.us:8080", "foo-dweb.ipfs.pvt.k12.ma.us"},
67
+ {"localhost", "localhost"},
68
+ {"[::1]:8080", "::1"},
69
+ } {
70
+ out := stripPort(test.in)
71
+ if out != test.out {
72
+ t.Errorf("(%s): returned '%s', expected '%s'", test.in, out, test.out)
73
+ }
74
+ }
75
+
76
+}
77
+
78
+func TestKnownSubdomainDetails(t *testing.T) {
79
+ gwSpec := config.GatewaySpec{
80
+ UseSubdomains: true,
81
+ }
82
+ knownGateways := map[string]config.GatewaySpec{
83
+ "localhost": gwSpec,
84
+ "dweb.link": gwSpec,
85
+ "dweb.ipfs.pvt.k12.ma.us": gwSpec, // note the sneaky ".ipfs." ;-)
86
+ }
87
+
88
+ for _, test := range []struct {
89
+ // in:
90
+ hostHeader string
91
+ // out:
92
+ hostname string
93
+ ns string
94
+ rootID string
95
+ ok bool
96
+ }{
97
+ // no subdomain
98
+ {"127.0.0.1:8080", "", "", "", false},
99
+ {"[::1]:8080", "", "", "", false},
100
+ {"hey.look.example.com", "", "", "", false},
101
+ {"dweb.link", "", "", "", false},
102
+ // malformed Host header
103
+ {".....dweb.link", "", "", "", false},
104
+ {"link", "", "", "", false},
105
+ {"8080:dweb.link", "", "", "", false},
106
+ {" ", "", "", "", false},
107
+ {"", "", "", "", false},
108
+ // unknown gateway host
109
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.unknown.example.com", "", "", "", false},
110
+ // cid in subdomain, known gateway
111
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.localhost:8080", "localhost:8080", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
112
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.dweb.link", "dweb.link", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
113
+ // capture everything before .ipfs.
114
+ {"foo.bar.boo-buzz.ipfs.dweb.link", "dweb.link", "ipfs", "foo.bar.boo-buzz", true},
115
+ // ipns
116
+ {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.localhost:8080", "localhost:8080", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true},
117
+ {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.dweb.link", "dweb.link", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true},
118
+ // edge case check: public gateway under long TLD (see: https://publicsuffix.org)
119
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.dweb.ipfs.pvt.k12.ma.us", "dweb.ipfs.pvt.k12.ma.us", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
120
+ {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.dweb.ipfs.pvt.k12.ma.us", "dweb.ipfs.pvt.k12.ma.us", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true},
121
+ // dnslink in subdomain
122
+ {"en.wikipedia-on-ipfs.org.ipns.localhost:8080", "localhost:8080", "ipns", "en.wikipedia-on-ipfs.org", true},
123
+ {"en.wikipedia-on-ipfs.org.ipns.localhost", "localhost", "ipns", "en.wikipedia-on-ipfs.org", true},
124
+ {"dist.ipfs.io.ipns.localhost:8080", "localhost:8080", "ipns", "dist.ipfs.io", true},
125
+ {"en.wikipedia-on-ipfs.org.ipns.dweb.link", "dweb.link", "ipns", "en.wikipedia-on-ipfs.org", true},
126
+ // edge case check: public gateway under long TLD (see: https://publicsuffix.org)
127
+ {"foo.dweb.ipfs.pvt.k12.ma.us", "", "", "", false},
128
+ {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.dweb.ipfs.pvt.k12.ma.us", "dweb.ipfs.pvt.k12.ma.us", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
129
+ {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.dweb.ipfs.pvt.k12.ma.us", "dweb.ipfs.pvt.k12.ma.us", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true},
130
+ // other namespaces
131
+ {"api.localhost", "", "", "", false},
132
+ {"peerid.p2p.localhost", "localhost", "p2p", "peerid", true},
133
+ } {
134
+ gw, hostname, ns, rootID, ok := knownSubdomainDetails(test.hostHeader, knownGateways)
135
+ if ok != test.ok {
136
+ t.Errorf("knownSubdomainDetails(%s): ok is %t, expected %t", test.hostHeader, ok, test.ok)
137
+ }
138
+ if rootID != test.rootID {
139
+ t.Errorf("knownSubdomainDetails(%s): rootID is '%s', expected '%s'", test.hostHeader, rootID, test.rootID)
140
+ }
141
+ if ns != test.ns {
142
+ t.Errorf("knownSubdomainDetails(%s): ns is '%s', expected '%s'", test.hostHeader, ns, test.ns)
143
+ }
144
+ if hostname != test.hostname {
145
+ t.Errorf("knownSubdomainDetails(%s): hostname is '%s', expected '%s'", test.hostHeader, hostname, test.hostname)
146
+ }
147
+ if ok && gw.UseSubdomains != gwSpec.UseSubdomains {
148
+ t.Errorf("knownSubdomainDetails(%s): gw is %+v, expected %+v", test.hostHeader, gw, gwSpec)
149
+ }
150
+ }
151
+
152
+}
core/corehttp/ipns_hostname.go
deleted
-38
@@ -1,38 +0,0 @@
1
-package corehttp
2
-
3
-import (
4
- "context"
5
- "net"
6
- "net/http"
7
- "strings"
8
-
9
- core "github.com/ipfs/go-ipfs/core"
10
- namesys "github.com/ipfs/go-ipfs/namesys"
11
-
12
- nsopts "github.com/ipfs/interface-go-ipfs-core/options/namesys"
13
- isd "github.com/jbenet/go-is-domain"
14
-)
15
-
16
-// IPNSHostnameOption rewrites an incoming request if its Host: header contains
17
-// an IPNS name.
18
-// The rewritten request points at the resolved name on the gateway handler.
19
-func IPNSHostnameOption() ServeOption {
20
- return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
21
- childMux := http.NewServeMux()
22
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
23
- ctx, cancel := context.WithCancel(n.Context())
24
- defer cancel()
25
-
26
- host := strings.SplitN(r.Host, ":", 2)[0]
27
- if len(host) > 0 && isd.IsDomain(host) {
28
- name := "/ipns/" + host
29
- _, err := n.Namesys.Resolve(ctx, name, nsopts.Depth(1))
30
- if err == nil || err == namesys.ErrResolveRecursion {
31
- r.URL.Path = name + r.URL.Path
32
- }
33
- }
34
- childMux.ServeHTTP(w, r)
35
- })
36
- return childMux, nil
37
- }
38
-}
docs/config.md
+150
-1
@@ -83,10 +83,13 @@ Available profiles:
83
- [`Routing.Type`](#routingtype)
84
- [`Gateway`](#gateway)
85
- [`Gateway.NoFetch`](#gatewaynofetch)
86
+ - [`Gateway.NoDNSLink`](#gatewaynodnslink)
87
- [`Gateway.HTTPHeaders`](#gatewayhttpheaders)
88
- [`Gateway.RootRedirect`](#gatewayrootredirect)
89
- [`Gateway.Writable`](#gatewaywritable)
90
- [`Gateway.PathPrefixes`](#gatewaypathprefixes)
91
+ - [`Gateway.PublicGateways`](#gatewaypublicgateways)
92
+ - [`Gateway` recipes](#gateway-recipes)
93
- [`Identity`](#identity)
94
- [`Identity.PeerID`](#identitypeerid)
95
- [`Identity.PrivKey`](#identityprivkey)
@@ -348,6 +351,14 @@ and will not fetch files from the network.
351
352
Default: `false`
353
354
+### `Gateway.NoDNSLink`
355
+
356
+A boolean to configure whether DNSLink lookup for value in `Host` HTTP header
357
+should be performed. If DNSLink is present, content path stored in the DNS TXT
358
+record becomes the `/` and respective payload is returned to the client.
359
+
360
+Default: `false`
361
+
362
### `Gateway.HTTPHeaders`
363
364
Headers to set on gateway responses.
@@ -379,7 +390,6 @@ A boolean to configure whether the gateway is writeable or not.
390
391
Default: `false`
392
382
-
393
### `Gateway.PathPrefixes`
394
395
Array of acceptable url paths that a client can specify in X-Ipfs-Path-Prefix
@@ -409,6 +419,145 @@ location /blog/ {
419
420
Default: `[]`
421
422
+
423
+### `Gateway.PublicGateways`
424
+
425
+`PublicGateways` is a dictionary for defining gateway behavior on specified hostnames.
426
+
427
+#### `Gateway.PublicGateways: Paths`
428
+
429
+Array of paths that should be exposed on the hostname.
430
+
431
+Example:
432
+```json
433
+{
434
+ "Gateway": {
435
+ "PublicGateways": {
436
+ "example.com": {
437
+ "Paths": ["/ipfs", "/ipns"],
438
+```
439
+
440
+Above enables `http://example.com/ipfs/*` and `http://example.com/ipns/*` but not `http://example.com/api/*`
441
+
442
+Default: `[]`
443
+
444
+#### `Gateway.PublicGateways: UseSubdomains`
445
+
446
+A boolean to configure whether the gateway at the hostname provides [Origin isolation](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy)
447
+between content roots.
448
+
449
+- `true` - enables [subdomain gateway](#https://docs-beta.ipfs.io/how-to/address-ipfs-on-web/#subdomain-gateway) at `http://*.{hostname}/`
450
+ - **Requires whitelist:** make sure respective `Paths` are set.
451
+ For example, `Paths: ["/ipfs", "/ipns"]` are required for `http://{cid}.ipfs.{hostname}` and `http://{foo}.ipns.{hostname}` to work:
452
+ ```json
453
+ {
454
+ "Gateway": {
455
+ "PublicGateways": {
456
+ "dweb.link": {
457
+ "UseSubdomains": true,
458
+ "Paths": ["/ipfs", "/ipns"],
459
+ ```
460
+ - **Backward-compatible:** requests for content paths such as `http://{hostname}/ipfs/{cid}` produce redirect to `http://{cid}.ipfs.{hostname}`
461
+ - **API:** if `/api` is on the `Paths` whitelist, `http://{hostname}/api/{cmd}` produces redirect to `http://api.{hostname}/api/{cmd}`
462
+
463
+- `false` - enables [path gateway](https://docs-beta.ipfs.io/how-to/address-ipfs-on-web/#path-gateway) at `http://{hostname}/*`
464
+ - Example:
465
+ ```json
466
+ {
467
+ "Gateway": {
468
+ "PublicGateways": {
469
+ "ipfs.io": {
470
+ "UseSubdomains": false,
471
+ "Paths": ["/ipfs", "/ipns", "/api"],
472
+ ```
473
+<!-- **(not implemented yet)** due to the lack of Origin isolation, cookies and storage on `Paths` will be disabled by [Clear-Site-Data](https://github.com/ipfs/in-web-browsers/issues/157) header -->
474
+
475
+Default: `false`
476
+
477
+
478
+#### `Gateway.PublicGateways: NoDNSLink`
479
+
480
+A boolean to configure whether DNSLink for hostname present in `Host`
481
+HTTP header should be resolved. Overrides global setting.
482
+If `Paths` are defined, they take priority over DNSLink.
483
+
484
+Default: `false` (DNSLink lookup enabled by default for every defined hostname)
485
+
486
+#### Implicit defaults of `Gateway.PublicGateways`
487
+
488
+Default entries for `localhost` hostname and loopback IPs are always present.
489
+If additional config is provided for those hostnames, it will be merged on top of implicit values:
490
+```json
491
+{
492
+ "Gateway": {
493
+ "PublicGateways": {
494
+ "localhost": {
495
+ "Paths": ["/ipfs", "/ipns"],
496
+ "UseSubdomains": true
497
+ }
498
+ }
499
+ }
500
+}
501
+```
502
+
503
+It is also possible to remove a default by setting it to `null`.
504
+For example, to disable subdomain gateway on `localhost`
505
+and make that hostname act the same as `127.0.0.1`:
506
+
507
+```console
508
+$ ipfs config --json Gateway.PublicGateways '{"localhost": null }'
509
+```
510
+
511
+### `Gateway` recipes
512
+
513
+Below is a list of the most common public gateway setups.
514
+
515
+* Public [subdomain gateway](https://docs-beta.ipfs.io/how-to/address-ipfs-on-web/#subdomain-gateway) at `http://{cid}.ipfs.dweb.link` (each content root gets its own Origin)
516
+ ```console
517
+ $ ipfs config --json Gateway.PublicGateways '{
518
+ "dweb.link": {
519
+ "UseSubdomains": true,
520
+ "Paths": ["/ipfs", "/ipns"]
521
+ }
522
+ }'
523
+ ```
524
+ **Note:** this enables automatic redirects from content paths to subdomains
525
+ `http://dweb.link/ipfs/{cid}` → `http://{cid}.ipfs.dweb.link`
526
+
527
+* Public [path gateway](https://docs-beta.ipfs.io/how-to/address-ipfs-on-web/#path-gateway) at `http://ipfs.io/ipfs/{cid}` (no Origin separation)
528
+ ```console
529
+ $ ipfs config --json Gateway.PublicGateways '{
530
+ "ipfs.io": {
531
+ "UseSubdomains": false,
532
+ "Paths": ["/ipfs", "/ipns", "/api"]
533
+ }
534
+ }'
535
+ ```
536
+
537
+* Public [DNSLink](https://dnslink.io/) gateway resolving every hostname passed in `Host` header.
538
+ ```console
539
+ $ ipfs config --json Gateway.NoDNSLink true
540
+ ```
541
+ * Note that `NoDNSLink: false` is the default (it works out of the box unless set to `true` manually)
542
+
543
+* Hardened, site-specific [DNSLink gateway](https://docs-beta.ipfs.io/how-to/address-ipfs-on-web/#dnslink-gateway).
544
+ Disable fetching of remote data (`NoFetch: true`)
545
+ and resolving DNSLink at unknown hostnames (`NoDNSLink: true`).
546
+ Then, enable DNSLink gateway only for the specific hostname (for which data
547
+ is already present on the node), without exposing any content-addressing `Paths`:
548
+ "NoFetch": true,
549
+ "NoDNSLink": true,
550
+ ```console
551
+ $ ipfs config --json Gateway.NoFetch true
552
+ $ ipfs config --json Gateway.NoDNSLink true
553
+ $ ipfs config --json Gateway.PublicGateways '{
554
+ "en.wikipedia-on-ipfs.org": {
555
+ "NoDNSLink": false,
556
+ "Paths": []
557
+ }
558
+ }'
559
+ ```
560
+
561
## `Identity`
562
563
### `Identity.PeerID`
docs/environment-variables.md
+1
-1
@@ -84,7 +84,7 @@ Default: https://ipfs.io/ipfs/$something (depends on the IPFS version)
84
85
## `IPFS_NS_MAP`
86
87
-Prewarms namesys cache with static records for deteministic tests and debugging.
87
+Adds static namesys records for deteministic tests and debugging.
88
Useful for testing things like DNSLink without real DNS lookup.
89
90
Example:
go.mod
+1
-1
@@ -31,7 +31,7 @@ require (
31
github.com/ipfs/go-ipfs-blockstore v0.1.4
32
github.com/ipfs/go-ipfs-chunker v0.0.4
33
github.com/ipfs/go-ipfs-cmds v0.1.2
34
- github.com/ipfs/go-ipfs-config v0.2.1
34
+ github.com/ipfs/go-ipfs-config v0.3.0
35
github.com/ipfs/go-ipfs-ds-help v0.1.1
36
github.com/ipfs/go-ipfs-exchange-interface v0.0.1
37
github.com/ipfs/go-ipfs-exchange-offline v0.0.1
go.sum
+2
-6
@@ -268,14 +268,10 @@ github.com/ipfs/go-ipfs-chunker v0.0.1 h1:cHUUxKFQ99pozdahi+uSC/3Y6HeRpi9oTeUHbE
268
github.com/ipfs/go-ipfs-chunker v0.0.1/go.mod h1:tWewYK0we3+rMbOh7pPFGDyypCtvGcBFymgY4rSDLAw=
269
github.com/ipfs/go-ipfs-chunker v0.0.4 h1:nb2ZIgtOk0TxJ5KDBEk+sv6iqJTF/PHg6owN2xCrUjE=
270
github.com/ipfs/go-ipfs-chunker v0.0.4/go.mod h1:jhgdF8vxRHycr00k13FM8Y0E+6BoalYeobXmUyTreP8=
271
-github.com/ipfs/go-ipfs-cmds v0.1.1 h1:H9/BLf5rcsULHMj/x8gC0e5o+raYhqk1OQsfzbGMNM4=
272
-github.com/ipfs/go-ipfs-cmds v0.1.1/go.mod h1:k1zMXcOLtljA9iAnZHddbH69yVm5+weRL0snmMD/rK0=
273
-github.com/ipfs/go-ipfs-cmds v0.1.2-0.20200316211807-0c2a21b0dacc h1:HIG2l6XUnov+M6UwcUKKrwGc8Q+n9AYGbiGM4pK21SM=
274
-github.com/ipfs/go-ipfs-cmds v0.1.2-0.20200316211807-0c2a21b0dacc/go.mod h1:a9LyFOtQCnVc3BvbAgW+GrMXEuN29aLCNi3Wk0IM8wo=
271
github.com/ipfs/go-ipfs-cmds v0.1.2 h1:02FLzTA9jYRle/xdMWYwGwxu3gzC3GhPUaz35dH+FrY=
272
github.com/ipfs/go-ipfs-cmds v0.1.2/go.mod h1:a9LyFOtQCnVc3BvbAgW+GrMXEuN29aLCNi3Wk0IM8wo=
277
-github.com/ipfs/go-ipfs-config v0.2.1 h1:Mpyvdf9Zc8k3jg+sRe8e9iylYXHYXqFMuePUjAZQvsE=
278
-github.com/ipfs/go-ipfs-config v0.2.1/go.mod h1:zCKH1uf1XIvf67589BnQ5IAv/Pld2J3gQoQYvG8TK8w=
273
+github.com/ipfs/go-ipfs-config v0.3.0 h1:fGs3JBqB9ia/Joi8up47uiKn150EOEqqVFwv8HZqXao=
274
+github.com/ipfs/go-ipfs-config v0.3.0/go.mod h1:nSLCFtlaL+2rbl3F+9D4gQZQbT1LjRKx7TJg/IHz6oM=
275
github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
276
github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ=
277
github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
namesys/namesys.go
+20
-3
@@ -2,11 +2,13 @@ package namesys
2
3
import (
4
"context"
5
+ "fmt"
6
"os"
7
"strings"
8
"time"
9
10
lru "github.com/hashicorp/golang-lru"
11
+ cid "github.com/ipfs/go-cid"
12
ds "github.com/ipfs/go-datastore"
13
path "github.com/ipfs/go-path"
14
opts "github.com/ipfs/interface-go-ipfs-core/options/namesys"
@@ -14,7 +16,6 @@ import (
16
ci "github.com/libp2p/go-libp2p-core/crypto"
17
peer "github.com/libp2p/go-libp2p-core/peer"
18
routing "github.com/libp2p/go-libp2p-core/routing"
17
- mh "github.com/multiformats/go-multihash"
19
)
20
21
// mpns (a multi-protocol NameSystem) implements generic IPFS naming.
@@ -133,12 +134,28 @@ func (ns *mpns) resolveOnceAsync(ctx context.Context, name string, options opts.
134
}
135
136
// Resolver selection:
136
- // 1. if it is a multihash resolve through "ipns".
137
+ // 1. if it is a PeerID/CID/multihash resolve through "ipns".
138
// 2. if it is a domain name, resolve through "dns"
139
// 3. otherwise resolve through the "proquint" resolver
140
141
var res resolver
141
- if _, err := mh.FromB58String(key); err == nil {
142
+ _, err := peer.Decode(key)
143
+
144
+ // CIDs in IPNS are expected to have libp2p-key multicodec
145
+ // We ease the transition by returning a more meaningful error with a valid CID
146
+ if err != nil && err.Error() == "can't convert CID of type protobuf to a peer ID" {
147
+ ipnsCid, cidErr := cid.Decode(key)
148
+ if cidErr == nil && ipnsCid.Version() == 1 && ipnsCid.Type() != cid.Libp2pKey {
149
+ fixedCid := cid.NewCidV1(cid.Libp2pKey, ipnsCid.Hash()).String()
150
+ codecErr := fmt.Errorf("peer ID represented as CIDv1 require libp2p-key multicodec: retry with /ipns/%s", fixedCid)
151
+ log.Debugf("RoutingResolver: could not convert public key hash %s to peer ID: %s\n", key, codecErr)
152
+ out <- onceResult{err: codecErr}
153
+ close(out)
154
+ return out
155
+ }
156
+ }
157
+
158
+ if err == nil {
159
res = ns.ipnsResolver
160
} else if isd.IsDomain(key) {
161
res = ns.dnsResolver
namesys/namesys_test.go
+9
-5
@@ -11,7 +11,7 @@ import (
11
offroute "github.com/ipfs/go-ipfs-routing/offline"
12
ipns "github.com/ipfs/go-ipns"
13
path "github.com/ipfs/go-path"
14
- "github.com/ipfs/go-unixfs"
14
+ unixfs "github.com/ipfs/go-unixfs"
15
opts "github.com/ipfs/interface-go-ipfs-core/options/namesys"
16
ci "github.com/libp2p/go-libp2p-core/crypto"
17
peer "github.com/libp2p/go-libp2p-core/peer"
@@ -49,10 +49,12 @@ func (r *mockResolver) resolveOnceAsync(ctx context.Context, name string, option
49
func mockResolverOne() *mockResolver {
50
return &mockResolver{
51
entries: map[string]string{
52
- "QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy": "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj",
53
- "QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n": "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy",
54
- "QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD": "/ipns/ipfs.io",
55
- "QmQ4QZh8nrsczdUEwTyfBope4THUhqxqc1fx6qYhhzZQei": "/ipfs/QmP3ouCnU8NNLsW6261pAx2pNLV2E4dQoisB1sgda12Act",
52
+ "QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy": "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj",
53
+ "QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n": "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy",
54
+ "QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD": "/ipns/ipfs.io",
55
+ "QmQ4QZh8nrsczdUEwTyfBope4THUhqxqc1fx6qYhhzZQei": "/ipfs/QmP3ouCnU8NNLsW6261pAx2pNLV2E4dQoisB1sgda12Act",
56
+ "12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5": "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", // ed25519+identity multihash
57
+ "bafzbeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm": "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", // cidv1 in base32 with libp2p-key multicodec
58
},
59
}
60
}
@@ -82,6 +84,8 @@ func TestNamesysResolution(t *testing.T) {
84
testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 1, "/ipns/ipfs.io", ErrResolveRecursion)
85
testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 2, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", ErrResolveRecursion)
86
testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 3, "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", ErrResolveRecursion)
87
+ testResolution(t, r, "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", 1, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", ErrResolveRecursion)
88
+ testResolution(t, r, "/ipns/bafzbeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", 1, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", ErrResolveRecursion)
89
}
90
91
func TestPublishWithCache0(t *testing.T) {
namesys/routing.go
+1
@@ -59,6 +59,7 @@ func (r *IpnsResolver) resolveOnceAsync(ctx context.Context, name string, option
59
}
60
61
name = strings.TrimPrefix(name, "/ipns/")
62
+
63
pid, err := peer.Decode(name)
64
if err != nil {
65
log.Debugf("RoutingResolver: could not convert public key hash %s to peer ID: %s\n", name, err)
test/sharness/t0111-gateway-writeable.sh
+10
-10
@@ -41,7 +41,7 @@ test_expect_success "HTTP gateway gives access to sample file" '
41
42
test_expect_success "HTTP POST file gives Hash" '
43
echo "$RANDOM" >infile &&
44
- URL="http://localhost:$port/ipfs/" &&
44
+ URL="http://127.0.0.1:$port/ipfs/" &&
45
curl -svX POST --data-binary @infile "$URL" 2>curl_post.out &&
46
grep "HTTP/1.1 201 Created" curl_post.out &&
47
LOCATION=$(grep Location curl_post.out) &&
@@ -49,7 +49,7 @@ test_expect_success "HTTP POST file gives Hash" '
49
'
50
51
test_expect_success "We can HTTP GET file just created" '
52
- URL="http://localhost:${port}${HASH}" &&
52
+ URL="http://127.0.0.1:${port}${HASH}" &&
53
curl -so outfile "$URL" &&
54
test_cmp infile outfile
55
'
@@ -60,7 +60,7 @@ test_expect_success "We got the correct hash" '
60
'
61
62
test_expect_success "HTTP GET empty directory" '
63
- URL="http://localhost:$port/ipfs/$HASH_EMPTY_DIR/" &&
63
+ URL="http://127.0.0.1:$port/ipfs/$HASH_EMPTY_DIR/" &&
64
echo "GET $URL" &&
65
curl -so outfile "$URL" 2>curl_getEmpty.out &&
66
grep "Index of /ipfs/$HASH_EMPTY_DIR/" outfile
@@ -68,7 +68,7 @@ test_expect_success "HTTP GET empty directory" '
68
69
test_expect_success "HTTP PUT file to construct a hierarchy" '
70
echo "$RANDOM" >infile &&
71
- URL="http://localhost:$port/ipfs/$HASH_EMPTY_DIR/test.txt" &&
71
+ URL="http://127.0.0.1:$port/ipfs/$HASH_EMPTY_DIR/test.txt" &&
72
echo "PUT $URL" &&
73
curl -svX PUT --data-binary @infile "$URL" 2>curl_put.out &&
74
grep "HTTP/1.1 201 Created" curl_put.out &&
@@ -77,7 +77,7 @@ test_expect_success "HTTP PUT file to construct a hierarchy" '
77
'
78
79
test_expect_success "We can HTTP GET file just created" '
80
- URL="http://localhost:$port/ipfs/$HASH/test.txt" &&
80
+ URL="http://127.0.0.1:$port/ipfs/$HASH/test.txt" &&
81
echo "GET $URL" &&
82
curl -so outfile "$URL" &&
83
test_cmp infile outfile
@@ -85,7 +85,7 @@ test_expect_success "We can HTTP GET file just created" '
85
86
test_expect_success "HTTP PUT file to append to existing hierarchy" '
87
echo "$RANDOM" >infile2 &&
88
- URL="http://localhost:$port/ipfs/$HASH/test/test.txt" &&
88
+ URL="http://127.0.0.1:$port/ipfs/$HASH/test/test.txt" &&
89
echo "PUT $URL" &&
90
curl -svX PUT --data-binary @infile2 "$URL" 2>curl_putAgain.out &&
91
grep "HTTP/1.1 201 Created" curl_putAgain.out &&
@@ -95,7 +95,7 @@ test_expect_success "HTTP PUT file to append to existing hierarchy" '
95
96
97
test_expect_success "We can HTTP GET file just updated" '
98
- URL="http://localhost:$port/ipfs/$HASH/test/test.txt" &&
98
+ URL="http://127.0.0.1:$port/ipfs/$HASH/test/test.txt" &&
99
echo "GET $URL" &&
100
curl -svo outfile2 "$URL" 2>curl_getAgain.out &&
101
test_cmp infile2 outfile2
@@ -103,7 +103,7 @@ test_expect_success "We can HTTP GET file just updated" '
103
104
test_expect_success "HTTP PUT to replace a directory" '
105
echo "$RANDOM" >infile3 &&
106
- URL="http://localhost:$port/ipfs/$HASH/test" &&
106
+ URL="http://127.0.0.1:$port/ipfs/$HASH/test" &&
107
echo "PUT $URL" &&
108
curl -svX PUT --data-binary @infile3 "$URL" 2>curl_putOverDirectory.out &&
109
grep "HTTP/1.1 201 Created" curl_putOverDirectory.out &&
@@ -112,7 +112,7 @@ test_expect_success "HTTP PUT to replace a directory" '
112
'
113
114
test_expect_success "We can HTTP GET file just put over a directory" '
115
- URL="http://localhost:$port/ipfs/$HASH/test" &&
115
+ URL="http://127.0.0.1:$port/ipfs/$HASH/test" &&
116
echo "GET $URL" &&
117
curl -svo outfile3 "$URL" 2>curl_getOverDirectory.out &&
118
test_cmp infile3 outfile3
@@ -120,7 +120,7 @@ test_expect_success "We can HTTP GET file just put over a directory" '
120
121
test_expect_success "HTTP PUT to /ipns fails" '
122
PEERID=`ipfs id --format="<id>"` &&
123
- URL="http://localhost:$port/ipns/$PEERID/test.txt" &&
123
+ URL="http://127.0.0.1:$port/ipns/$PEERID/test.txt" &&
124
echo "PUT $URL" &&
125
curl -svX PUT --data-binary @infile1 "$URL" 2>curl_putIpns.out &&
126
grep "HTTP/1.1 400 Bad Request" curl_putIpns.out
test/sharness/t0114-gateway-subdomains.sh
new
+641
@@ -0,0 +1,641 @@
1
+#!/usr/bin/env bash
2
+#
3
+# Copyright (c) Protocol Labs
4
+
5
+test_description="Test subdomain support on the HTTP gateway"
6
+
7
+
8
+. lib/test-lib.sh
9
+
10
+## ============================================================================
11
+## Helpers specific to subdomain tests
12
+## ============================================================================
13
+
14
+# Helper that tests gateway response over direct HTTP
15
+# and in all supported HTTP proxy modes
16
+test_localhost_gateway_response_should_contain() {
17
+ local label="$1"
18
+ local expected="$3"
19
+
20
+ # explicit "Host: $hostname" header to match browser behavior
21
+ # and also make tests independent from DNS
22
+ local host=$(echo $2 | cut -d'/' -f3 | cut -d':' -f1)
23
+ local hostname=$(echo $2 | cut -d'/' -f3 | cut -d':' -f1,2)
24
+
25
+ # Proxy is the same as HTTP Gateway, we use raw IP and port to be sure
26
+ local proxy="http://127.0.0.1:$GWAY_PORT"
27
+
28
+ # Create a raw URL version with IP to ensure hostname from Host header is used
29
+ # (removes false-positives, Host header is used for passing hostname already)
30
+ local url="$2"
31
+ local rawurl=$(echo "$url" | sed "s/$hostname/127.0.0.1:$GWAY_PORT/")
32
+
33
+ #echo "hostname: $hostname"
34
+ #echo "url before: $url"
35
+ #echo "url after: $rawurl"
36
+
37
+ # regular HTTP request
38
+ # (hostname in Host header, raw IP in URL)
39
+ test_expect_success "$label (direct HTTP)" "
40
+ curl -H \"Host: $hostname\" -sD - \"$rawurl\" > response &&
41
+ test_should_contain \"$expected\" response
42
+ "
43
+
44
+ # HTTP proxy
45
+ # (hostname is passed via URL)
46
+ # Note: proxy client should not care, but curl does DNS lookup
47
+ # for some reason anyway, so we pass static DNS mapping
48
+ test_expect_success "$label (HTTP proxy)" "
49
+ curl -x $proxy --resolve $hostname:127.0.0.1 -sD - \"$url\" > response &&
50
+ test_should_contain \"$expected\" response
51
+ "
52
+
53
+ # HTTP proxy 1.0
54
+ # (repeating proxy test with older spec, just to be sure)
55
+ test_expect_success "$label (HTTP proxy 1.0)" "
56
+ curl --proxy1.0 $proxy --resolve $hostname:127.0.0.1 -sD - \"$url\" > response &&
57
+ test_should_contain \"$expected\" response
58
+ "
59
+
60
+ # HTTP proxy tunneling (CONNECT)
61
+ # https://tools.ietf.org/html/rfc7231#section-4.3.6
62
+ # In HTTP/1.x, the pseudo-method CONNECT
63
+ # can be used to convert an HTTP connection into a tunnel to a remote host
64
+ test_expect_success "$label (HTTP proxy tunneling)" "
65
+ curl --proxytunnel -x $proxy -H \"Host: $hostname\" -sD - \"$rawurl\" > response &&
66
+ test_should_contain \"$expected\" response
67
+ "
68
+}
69
+
70
+# Helper that checks gateway resonse for specific hostname in Host header
71
+test_hostname_gateway_response_should_contain() {
72
+ local label="$1"
73
+ local hostname="$2"
74
+ local url="$3"
75
+ local rawurl=$(echo "$url" | sed "s/$hostname/127.0.0.1:$GWAY_PORT/")
76
+ local expected="$4"
77
+ test_expect_success "$label" "
78
+ curl -H \"Host: $hostname\" -sD - \"$rawurl\" > response &&
79
+ test_should_contain \"$expected\" response
80
+ "
81
+}
82
+
83
+## ============================================================================
84
+## Start IPFS Node and prepare test CIDs
85
+## ============================================================================
86
+
87
+test_init_ipfs
88
+test_launch_ipfs_daemon --offline
89
+
90
+# CIDv0to1 is necessary because raw-leaves are enabled by default during
91
+# "ipfs add" with CIDv1 and disabled with CIDv0
92
+test_expect_success "Add test text file" '
93
+ CID_VAL="hello"
94
+ CIDv1=$(echo $CID_VAL | ipfs add --cid-version 1 -Q)
95
+ CIDv0=$(echo $CID_VAL | ipfs add --cid-version 0 -Q)
96
+ CIDv0to1=$(echo "$CIDv0" | ipfs cid base32)
97
+'
98
+
99
+test_expect_success "Add the test directory" '
100
+ mkdir -p testdirlisting/subdir1/subdir2 &&
101
+ echo "hello" > testdirlisting/hello &&
102
+ echo "subdir2-bar" > testdirlisting/subdir1/subdir2/bar &&
103
+ mkdir -p testdirlisting/api &&
104
+ mkdir -p testdirlisting/ipfs &&
105
+ echo "I am a txt file" > testdirlisting/api/file.txt &&
106
+ echo "I am a txt file" > testdirlisting/ipfs/file.txt &&
107
+ DIR_CID=$(ipfs add -Qr --cid-version 1 testdirlisting)
108
+'
109
+
110
+test_expect_success "Publish test text file to IPNS" '
111
+ PEERID=$(ipfs id --format="<id>")
112
+ IPNS_IDv0=$(echo "$PEERID" | ipfs cid format -v 0)
113
+ IPNS_IDv1=$(echo "$PEERID" | ipfs cid format -v 1 --codec libp2p-key -b base32)
114
+ IPNS_IDv1_DAGPB=$(echo "$IPNS_IDv0" | ipfs cid format -v 1 -b base32)
115
+ test_check_peerid "${PEERID}" &&
116
+ ipfs name publish --allow-offline -Q "/ipfs/$CIDv1" > name_publish_out &&
117
+ ipfs name resolve "$PEERID" > output &&
118
+ printf "/ipfs/%s\n" "$CIDv1" > expected2 &&
119
+ test_cmp expected2 output
120
+'
121
+
122
+
123
+# ensure we start with empty Gateway.PublicGateways
124
+test_expect_success 'start daemon with empty config for Gateway.PublicGateways' '
125
+ test_kill_ipfs_daemon &&
126
+ ipfs config --json Gateway.PublicGateways "{}" &&
127
+ test_launch_ipfs_daemon --offline
128
+'
129
+
130
+## ============================================================================
131
+## Test path-based requests to a local gateway with default config
132
+## (forced redirects to http://*.localhost)
133
+## ============================================================================
134
+
135
+# /ipfs/<cid>
136
+
137
+# IP remains old school path-based gateway
138
+
139
+test_localhost_gateway_response_should_contain \
140
+ "request for 127.0.0.1/ipfs/{CID} stays on path" \
141
+ "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \
142
+ "$CID_VAL"
143
+
144
+# 'localhost' hostname is used for subdomains, and should not return
145
+# payload directly, but redirect to URL with proper origin isolation
146
+
147
+test_localhost_gateway_response_should_contain \
148
+ "request for localhost/ipfs/{CIDv1} redirects to subdomain" \
149
+ "http://localhost:$GWAY_PORT/ipfs/$CIDv1" \
150
+ "Location: http://$CIDv1.ipfs.localhost:$GWAY_PORT/"
151
+
152
+test_localhost_gateway_response_should_contain \
153
+ "request for localhost/ipfs/{CIDv0} redirects to CIDv1 representation in subdomain" \
154
+ "http://localhost:$GWAY_PORT/ipfs/$CIDv0" \
155
+ "Location: http://${CIDv0to1}.ipfs.localhost:$GWAY_PORT/"
156
+
157
+# /ipns/<libp2p-key>
158
+
159
+test_localhost_gateway_response_should_contain \
160
+ "request for localhost/ipns/{CIDv0} redirects to CIDv1 with libp2p-key multicodec in subdomain" \
161
+ "http://localhost:$GWAY_PORT/ipns/$IPNS_IDv0" \
162
+ "Location: http://${IPNS_IDv1}.ipns.localhost:$GWAY_PORT/"
163
+
164
+# /ipns/<dnslink-fqdn>
165
+
166
+test_localhost_gateway_response_should_contain \
167
+ "request for localhost/ipns/{fqdn} redirects to DNSLink in subdomain" \
168
+ "http://localhost:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki" \
169
+ "Location: http://en.wikipedia-on-ipfs.org.ipns.localhost:$GWAY_PORT/wiki"
170
+
171
+# API on localhost subdomain gateway
172
+
173
+# /api/v0 present on the root hostname
174
+test_localhost_gateway_response_should_contain \
175
+ "request for localhost/api" \
176
+ "http://localhost:$GWAY_PORT/api/v0/refs?arg=${DIR_CID}&r=true" \
177
+ "Ref"
178
+
179
+# /api/v0 not mounted on content root subdomains
180
+test_localhost_gateway_response_should_contain \
181
+ "request for {cid}.ipfs.localhost/api returns data if present on the content root" \
182
+ "http://${DIR_CID}.ipfs.localhost:$GWAY_PORT/api/file.txt" \
183
+ "I am a txt file"
184
+
185
+test_localhost_gateway_response_should_contain \
186
+ "request for {cid}.ipfs.localhost/api/v0/refs returns 404" \
187
+ "http://${DIR_CID}.ipfs.localhost:$GWAY_PORT/api/v0/refs?arg=${DIR_CID}&r=true" \
188
+ "404 Not Found"
189
+
190
+## ============================================================================
191
+## Test subdomain-based requests to a local gateway with default config
192
+## (origin per content root at http://*.localhost)
193
+## ============================================================================
194
+
195
+# {CID}.ipfs.localhost
196
+
197
+test_localhost_gateway_response_should_contain \
198
+ "request for {CID}.ipfs.localhost should return expected payload" \
199
+ "http://${CIDv1}.ipfs.localhost:$GWAY_PORT" \
200
+ "$CID_VAL"
201
+
202
+# ensure /ipfs/ namespace is not mounted on subdomain
203
+test_localhost_gateway_response_should_contain \
204
+ "request for {CID}.ipfs.localhost/ipfs/{CID} should return HTTP 404" \
205
+ "http://${CIDv1}.ipfs.localhost:$GWAY_PORT/ipfs/$CIDv1" \
206
+ "404 Not Found"
207
+
208
+# ensure requests to /ipfs/* are not blocked, if content root has such subdirectory
209
+test_localhost_gateway_response_should_contain \
210
+ "request for {CID}.ipfs.localhost/ipfs/file.txt should return data from a file in CID content root" \
211
+ "http://${DIR_CID}.ipfs.localhost:$GWAY_PORT/ipfs/file.txt" \
212
+ "I am a txt file"
213
+
214
+# {CID}.ipfs.localhost/sub/dir (Directory Listing)
215
+DIR_HOSTNAME="${DIR_CID}.ipfs.localhost:$GWAY_PORT"
216
+
217
+test_expect_success "valid file and subdirectory paths in directory listing at {cid}.ipfs.localhost" '
218
+ curl -s --resolve $DIR_HOSTNAME:127.0.0.1 "http://$DIR_HOSTNAME" > list_response &&
219
+ test_should_contain "<a href=\"/hello\">hello</a>" list_response &&
220
+ test_should_contain "<a href=\"/subdir1\">subdir1</a>" list_response
221
+'
222
+
223
+test_expect_success "valid parent directory path in directory listing at {cid}.ipfs.localhost/sub/dir" '
224
+ curl -s --resolve $DIR_HOSTNAME:127.0.0.1 "http://$DIR_HOSTNAME/subdir1/subdir2/" > list_response &&
225
+ test_should_contain "<a href=\"/subdir1/subdir2/./..\">..</a>" list_response &&
226
+ test_should_contain "<a href=\"/subdir1/subdir2/bar\">bar</a>" list_response
227
+'
228
+
229
+test_expect_success "request for deep path resource at {cid}.ipfs.localhost/sub/dir/file" '
230
+ curl -s --resolve $DIR_HOSTNAME:127.0.0.1 "http://$DIR_HOSTNAME/subdir1/subdir2/bar" > list_response &&
231
+ test_should_contain "subdir2-bar" list_response
232
+'
233
+
234
+# *.ipns.localhost
235
+
236
+# <libp2p-key>.ipns.localhost
237
+
238
+test_localhost_gateway_response_should_contain \
239
+ "request for {CIDv1-libp2p-key}.ipns.localhost returns expected payload" \
240
+ "http://${IPNS_IDv1}.ipns.localhost:$GWAY_PORT" \
241
+ "$CID_VAL"
242
+
243
+test_localhost_gateway_response_should_contain \
244
+ "request for {CIDv1-dag-pb}.ipns.localhost redirects to CID with libp2p-key multicodec" \
245
+ "http://${IPNS_IDv1_DAGPB}.ipns.localhost:$GWAY_PORT" \
246
+ "Location: http://${IPNS_IDv1}.ipns.localhost:$GWAY_PORT/"
247
+
248
+# <dnslink-fqdn>.ipns.localhost
249
+
250
+# DNSLink test requires a daemon in online mode with precached /ipns/ mapping
251
+test_kill_ipfs_daemon
252
+DNSLINK_FQDN="dnslink-test.example.com"
253
+export IPFS_NS_MAP="$DNSLINK_FQDN:/ipfs/$CIDv1"
254
+test_launch_ipfs_daemon
255
+
256
+test_localhost_gateway_response_should_contain \
257
+ "request for {dnslink}.ipns.localhost returns expected payload" \
258
+ "http://$DNSLINK_FQDN.ipns.localhost:$GWAY_PORT" \
259
+ "$CID_VAL"
260
+
261
+# api.localhost/api
262
+
263
+# Note: we use DIR_CID so refs -r returns some CIDs for child nodes
264
+test_localhost_gateway_response_should_contain \
265
+ "request for api.localhost returns API response" \
266
+ "http://api.localhost:$GWAY_PORT/api/v0/refs?arg=$DIR_CID&r=true" \
267
+ "Ref"
268
+
269
+## ============================================================================
270
+## Test subdomain-based requests with a custom hostname config
271
+## (origin per content root at http://*.example.com)
272
+## ============================================================================
273
+
274
+# set explicit subdomain gateway config for the hostname
275
+ipfs config --json Gateway.PublicGateways '{
276
+ "example.com": {
277
+ "UseSubdomains": true,
278
+ "Paths": ["/ipfs", "/ipns", "/api"]
279
+ }
280
+}' || exit 1
281
+# restart daemon to apply config changes
282
+test_kill_ipfs_daemon
283
+test_launch_ipfs_daemon --offline
284
+
285
+
286
+# example.com/ip(f|n)s/*
287
+# =============================================================================
288
+
289
+# path requests to the root hostname should redirect
290
+# to a subdomain URL with proper origin isolation
291
+
292
+test_hostname_gateway_response_should_contain \
293
+ "request for example.com/ipfs/{CIDv1} produces redirect to {CIDv1}.ipfs.example.com" \
294
+ "example.com" \
295
+ "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \
296
+ "Location: http://$CIDv1.ipfs.example.com/"
297
+
298
+# error message should include original CID
299
+# (and it should be case-sensitive, as we can't assume everyone uses base32)
300
+test_hostname_gateway_response_should_contain \
301
+ "request for example.com/ipfs/{InvalidCID} produces useful error before redirect" \
302
+ "example.com" \
303
+ "http://127.0.0.1:$GWAY_PORT/ipfs/QmInvalidCID" \
304
+ 'invalid path \"/ipfs/QmInvalidCID\"'
305
+
306
+test_hostname_gateway_response_should_contain \
307
+ "request for example.com/ipfs/{CIDv0} produces redirect to {CIDv1}.ipfs.example.com" \
308
+ "example.com" \
309
+ "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv0" \
310
+ "Location: http://${CIDv0to1}.ipfs.example.com/"
311
+
312
+# Support X-Forwarded-Proto
313
+test_expect_success "request for http://example.com/ipfs/{CID} with X-Forwarded-Proto: https produces redirect to HTTPS URL" "
314
+ curl -H \"X-Forwarded-Proto: https\" -H \"Host: example.com\" -sD - \"http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1\" > response &&
315
+ test_should_contain \"Location: https://$CIDv1.ipfs.example.com/\" response
316
+"
317
+
318
+
319
+
320
+# example.com/ipns/<libp2p-key>
321
+
322
+test_hostname_gateway_response_should_contain \
323
+ "request for example.com/ipns/{CIDv0} redirects to CIDv1 with libp2p-key multicodec in subdomain" \
324
+ "example.com" \
325
+ "http://127.0.0.1:$GWAY_PORT/ipns/$IPNS_IDv0" \
326
+ "Location: http://${IPNS_IDv1}.ipns.example.com/"
327
+
328
+# example.com/ipns/<dnslink-fqdn>
329
+
330
+test_hostname_gateway_response_should_contain \
331
+ "request for example.com/ipns/{fqdn} redirects to DNSLink in subdomain" \
332
+ "example.com" \
333
+ "http://127.0.0.1:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki" \
334
+ "Location: http://en.wikipedia-on-ipfs.org.ipns.example.com/wiki"
335
+
336
+# *.ipfs.example.com: subdomain requests made with custom FQDN in Host header
337
+
338
+test_hostname_gateway_response_should_contain \
339
+ "request for {CID}.ipfs.example.com should return expected payload" \
340
+ "${CIDv1}.ipfs.example.com" \
341
+ "http://127.0.0.1:$GWAY_PORT/" \
342
+ "$CID_VAL"
343
+
344
+test_hostname_gateway_response_should_contain \
345
+ "request for {CID}.ipfs.example.com/ipfs/{CID} should return HTTP 404" \
346
+ "${CIDv1}.ipfs.example.com" \
347
+ "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \
348
+ "404 Not Found"
349
+
350
+# {CID}.ipfs.example.com/sub/dir (Directory Listing)
351
+DIR_FQDN="${DIR_CID}.ipfs.example.com"
352
+
353
+test_expect_success "valid file and directory paths in directory listing at {cid}.ipfs.example.com" '
354
+ curl -s -H "Host: $DIR_FQDN" http://127.0.0.1:$GWAY_PORT > list_response &&
355
+ test_should_contain "<a href=\"/hello\">hello</a>" list_response &&
356
+ test_should_contain "<a href=\"/subdir1\">subdir1</a>" list_response
357
+'
358
+
359
+test_expect_success "valid parent directory path in directory listing at {cid}.ipfs.example.com/sub/dir" '
360
+ curl -s -H "Host: $DIR_FQDN" http://127.0.0.1:$GWAY_PORT/subdir1/subdir2/ > list_response &&
361
+ test_should_contain "<a href=\"/subdir1/subdir2/./..\">..</a>" list_response &&
362
+ test_should_contain "<a href=\"/subdir1/subdir2/bar\">bar</a>" list_response
363
+'
364
+
365
+test_expect_success "request for deep path resource {cid}.ipfs.example.com/sub/dir/file" '
366
+ curl -s -H "Host: $DIR_FQDN" http://127.0.0.1:$GWAY_PORT/subdir1/subdir2/bar > list_response &&
367
+ test_should_contain "subdir2-bar" list_response
368
+'
369
+
370
+# *.ipns.example.com
371
+# ============================================================================
372
+
373
+# <libp2p-key>.ipns.example.com
374
+
375
+test_hostname_gateway_response_should_contain \
376
+ "request for {CIDv1-libp2p-key}.ipns.example.com returns expected payload" \
377
+ "${IPNS_IDv1}.ipns.example.com" \
378
+ "http://127.0.0.1:$GWAY_PORT" \
379
+ "$CID_VAL"
380
+
381
+test_hostname_gateway_response_should_contain \
382
+ "request for {CIDv1-dag-pb}.ipns.localhost redirects to CID with libp2p-key multicodec" \
383
+ "${IPNS_IDv1_DAGPB}.ipns.example.com" \
384
+ "http://127.0.0.1:$GWAY_PORT" \
385
+ "Location: http://${IPNS_IDv1}.ipns.example.com/"
386
+
387
+# API on subdomain gateway example.com
388
+# ============================================================================
389
+
390
+# present at the root domain
391
+test_hostname_gateway_response_should_contain \
392
+ "request for example.com/api/v0/refs returns expected payload when /api is on Paths whitelist" \
393
+ "example.com" \
394
+ "http://127.0.0.1:$GWAY_PORT/api/v0/refs?arg=${DIR_CID}&r=true" \
395
+ "Ref"
396
+
397
+# not mounted on content root subdomains
398
+test_hostname_gateway_response_should_contain \
399
+ "request for {cid}.ipfs.example.com/api returns data if present on the content root" \
400
+ "$DIR_CID.ipfs.example.com" \
401
+ "http://127.0.0.1:$GWAY_PORT/api/file.txt" \
402
+ "I am a txt file"
403
+
404
+test_hostname_gateway_response_should_contain \
405
+ "request for {cid}.ipfs.example.com/api/v0/refs returns 404" \
406
+ "$CIDv1.ipfs.example.com" \
407
+ "http://127.0.0.1:$GWAY_PORT/api/v0/refs?arg=${DIR_CID}&r=true" \
408
+ "404 Not Found"
409
+
410
+# disable /api on example.com
411
+ipfs config --json Gateway.PublicGateways '{
412
+ "example.com": {
413
+ "UseSubdomains": true,
414
+ "Paths": ["/ipfs", "/ipns"]
415
+ }
416
+}' || exit 1
417
+# restart daemon to apply config changes
418
+test_kill_ipfs_daemon
419
+test_launch_ipfs_daemon --offline
420
+
421
+# not mounted at the root domain
422
+test_hostname_gateway_response_should_contain \
423
+ "request for example.com/api/v0/refs returns 404 if /api not on Paths whitelist" \
424
+ "example.com" \
425
+ "http://127.0.0.1:$GWAY_PORT/api/v0/refs?arg=${DIR_CID}&r=true" \
426
+ "404 Not Found"
427
+
428
+# not mounted on content root subdomains
429
+test_hostname_gateway_response_should_contain \
430
+ "request for {cid}.ipfs.example.com/api returns data if present on the content root" \
431
+ "$DIR_CID.ipfs.example.com" \
432
+ "http://127.0.0.1:$GWAY_PORT/api/file.txt" \
433
+ "I am a txt file"
434
+
435
+# DNSLink: <dnslink-fqdn>.ipns.example.com
436
+# (not really useful outside of localhost, as setting TLS for more than one
437
+# level of wildcard is a pain, but we support it if someone really wants it)
438
+# ============================================================================
439
+
440
+# DNSLink test requires a daemon in online mode with precached /ipns/ mapping
441
+test_kill_ipfs_daemon
442
+DNSLINK_FQDN="dnslink-subdomain-gw-test.example.org"
443
+export IPFS_NS_MAP="$DNSLINK_FQDN:/ipfs/$CIDv1"
444
+test_launch_ipfs_daemon
445
+
446
+test_hostname_gateway_response_should_contain \
447
+ "request for {dnslink}.ipns.example.com returns expected payload" \
448
+ "$DNSLINK_FQDN.ipns.example.com" \
449
+ "http://127.0.0.1:$GWAY_PORT" \
450
+ "$CID_VAL"
451
+
452
+# Disable selected Paths for the subdomain gateway hostname
453
+# =============================================================================
454
+
455
+# disable /ipns for the hostname by not whitelisting it
456
+ipfs config --json Gateway.PublicGateways '{
457
+ "example.com": {
458
+ "UseSubdomains": true,
459
+ "Paths": ["/ipfs"]
460
+ }
461
+}' || exit 1
462
+# restart daemon to apply config changes
463
+test_kill_ipfs_daemon
464
+test_launch_ipfs_daemon --offline
465
+
466
+# refuse requests to Paths that were not explicitly whitelisted for the hostname
467
+test_hostname_gateway_response_should_contain \
468
+ "request for *.ipns.example.com returns HTTP 404 Not Found when /ipns is not on Paths whitelist" \
469
+ "${IPNS_IDv1}.ipns.example.com" \
470
+ "http://127.0.0.1:$GWAY_PORT" \
471
+ "404 Not Found"
472
+
473
+
474
+## ============================================================================
475
+## Test path-based requests with a custom hostname config
476
+## ============================================================================
477
+
478
+# set explicit subdomain gateway config for the hostname
479
+ipfs config --json Gateway.PublicGateways '{
480
+ "example.com": {
481
+ "UseSubdomains": false,
482
+ "Paths": ["/ipfs"]
483
+ }
484
+}' || exit 1
485
+
486
+# restart daemon to apply config changes
487
+test_kill_ipfs_daemon
488
+test_launch_ipfs_daemon --offline
489
+
490
+# example.com/ip(f|n)s/* smoke-tests
491
+# =============================================================================
492
+
493
+# confirm path gateway works for /ipfs
494
+test_hostname_gateway_response_should_contain \
495
+ "request for example.com/ipfs/{CIDv1} returns expected payload" \
496
+ "example.com" \
497
+ "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \
498
+ "$CID_VAL"
499
+
500
+# refuse subdomain requests on path gateway
501
+# (we don't want false sense of security)
502
+test_hostname_gateway_response_should_contain \
503
+ "request for {CID}.ipfs.example.com/ipfs/{CID} should return HTTP 404 Not Found" \
504
+ "${CIDv1}.ipfs.example.com" \
505
+ "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \
506
+ "404 Not Found"
507
+
508
+# refuse requests to Paths that were not explicitly whitelisted for the hostname
509
+test_hostname_gateway_response_should_contain \
510
+ "request for example.com/ipns/ returns HTTP 404 Not Found when /ipns is not on Paths whitelist" \
511
+ "example.com" \
512
+ "http://127.0.0.1:$GWAY_PORT/ipns/$IPNS_IDv1" \
513
+ "404 Not Found"
514
+
515
+## ============================================================================
516
+## Test DNSLink requests with a custom PublicGateway (hostname config)
517
+## (DNSLink site at http://dnslink-test.example.com)
518
+## ============================================================================
519
+
520
+test_kill_ipfs_daemon
521
+
522
+# disable wildcard DNSLink gateway
523
+# and enable it on specific NSLink hostname
524
+ipfs config --json Gateway.NoDNSLink true && \
525
+ipfs config --json Gateway.PublicGateways '{
526
+ "dnslink-enabled-on-fqdn.example.org": {
527
+ "NoDNSLink": false,
528
+ "UseSubdomains": false,
529
+ "Paths": ["/ipfs"]
530
+ },
531
+ "only-dnslink-enabled-on-fqdn.example.org": {
532
+ "NoDNSLink": false,
533
+ "UseSubdomains": false,
534
+ "Paths": []
535
+ },
536
+ "dnslink-disabled-on-fqdn.example.com": {
537
+ "NoDNSLink": true,
538
+ "UseSubdomains": false,
539
+ "Paths": []
540
+ }
541
+}' || exit 1
542
+
543
+# DNSLink test requires a daemon in online mode with precached /ipns/ mapping
544
+DNSLINK_FQDN="dnslink-enabled-on-fqdn.example.org"
545
+ONLY_DNSLINK_FQDN="only-dnslink-enabled-on-fqdn.example.org"
546
+NO_DNSLINK_FQDN="dnslink-disabled-on-fqdn.example.com"
547
+export IPFS_NS_MAP="$DNSLINK_FQDN:/ipfs/$CIDv1,$ONLY_DNSLINK_FQDN:/ipfs/$DIR_CID"
548
+
549
+# restart daemon to apply config changes
550
+test_launch_ipfs_daemon
551
+
552
+# make sure test setup is valid (fail if CoreAPI is unable to resolve)
553
+test_expect_success "spoofed DNSLink record resolves in cli" "
554
+ ipfs resolve /ipns/$DNSLINK_FQDN > result &&
555
+ test_should_contain \"$CIDv1\" result &&
556
+ ipfs cat /ipns/$DNSLINK_FQDN > result &&
557
+ test_should_contain \"$CID_VAL\" result
558
+"
559
+
560
+# DNSLink enabled
561
+
562
+test_hostname_gateway_response_should_contain \
563
+ "request for http://{dnslink-fqdn}/ PublicGateway returns expected payload" \
564
+ "$DNSLINK_FQDN" \
565
+ "http://127.0.0.1:$GWAY_PORT/" \
566
+ "$CID_VAL"
567
+
568
+test_hostname_gateway_response_should_contain \
569
+ "request for {dnslink-fqdn}/ipfs/{cid} returns expected payload when /ipfs is on Paths whitelist" \
570
+ "$DNSLINK_FQDN" \
571
+ "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \
572
+ "$CID_VAL"
573
+
574
+# Test for a fun edge case: DNSLink-only gateway without /ipfs/ namespace
575
+# mounted, and with subdirectory named "ipfs" ¯\_(ツ)_/¯
576
+test_hostname_gateway_response_should_contain \
577
+ "request for {dnslink-fqdn}/ipfs/file.txt returns data from content root when /ipfs in not on Paths whitelist" \
578
+ "$ONLY_DNSLINK_FQDN" \
579
+ "http://127.0.0.1:$GWAY_PORT/ipfs/file.txt" \
580
+ "I am a txt file"
581
+
582
+test_hostname_gateway_response_should_contain \
583
+ "request for {dnslink-fqdn}/ipns/{peerid} returns 404 when path is not whitelisted" \
584
+ "$DNSLINK_FQDN" \
585
+ "http://127.0.0.1:$GWAY_PORT/ipns/$IPNS_IDv0" \
586
+ "404 Not Found"
587
+
588
+# DNSLink disabled
589
+
590
+test_hostname_gateway_response_should_contain \
591
+ "request for http://{dnslink-fqdn}/ returns 404 when NoDNSLink=true" \
592
+ "$NO_DNSLINK_FQDN" \
593
+ "http://127.0.0.1:$GWAY_PORT/" \
594
+ "404 Not Found"
595
+
596
+test_hostname_gateway_response_should_contain \
597
+ "request for {dnslink-fqdn}/ipfs/{cid} returns 404 when path is not whitelisted" \
598
+ "$NO_DNSLINK_FQDN" \
599
+ "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv0" \
600
+ "404 Not Found"
601
+
602
+
603
+## ============================================================================
604
+## Test wildcard DNSLink (any hostname, with default config)
605
+## ============================================================================
606
+
607
+test_kill_ipfs_daemon
608
+
609
+# enable wildcard DNSLink gateway (any value in Host header)
610
+# and remove custom PublicGateways
611
+ipfs config --json Gateway.NoDNSLink false && \
612
+ipfs config --json Gateway.PublicGateways '{}' || exit 1
613
+
614
+# DNSLink test requires a daemon in online mode with precached /ipns/ mapping
615
+DNSLINK_FQDN="wildcard-dnslink-not-in-config.example.com"
616
+export IPFS_NS_MAP="$DNSLINK_FQDN:/ipfs/$CIDv1"
617
+
618
+# restart daemon to apply config changes
619
+test_launch_ipfs_daemon
620
+
621
+# make sure test setup is valid (fail if CoreAPI is unable to resolve)
622
+test_expect_success "spoofed DNSLink record resolves in cli" "
623
+ ipfs resolve /ipns/$DNSLINK_FQDN > result &&
624
+ test_should_contain \"$CIDv1\" result &&
625
+ ipfs cat /ipns/$DNSLINK_FQDN > result &&
626
+ test_should_contain \"$CID_VAL\" result
627
+"
628
+
629
+# gateway test
630
+test_hostname_gateway_response_should_contain \
631
+ "request for http://{dnslink-fqdn}/ (wildcard) returns expected payload" \
632
+ "$DNSLINK_FQDN" \
633
+ "http://127.0.0.1:$GWAY_PORT/" \
634
+ "$CID_VAL"
635
+
636
+# =============================================================================
637
+# ensure we end with empty Gateway.PublicGateways
638
+ipfs config --json Gateway.PublicGateways '{}'
639
+test_kill_ipfs_daemon
640
+
641
+test_done
test/sharness/t0160-resolve.sh
+9
@@ -116,6 +116,15 @@ test_resolve_cmd_b32() {
116
117
test_resolve_setup_name "self" "/ipfs/$c_hash_b32"
118
test_resolve "/ipns/$self_hash" "/ipfs/$c_hash_b32" --cid-base=base32
119
+
120
+ # peer ID represented as CIDv1 require libp2p-key multicodec
121
+ # https://github.com/libp2p/specs/blob/master/RFC/0001-text-peerid-cid.md
122
+ local self_hash_b32protobuf=$(echo $self_hash | ipfs cid format -v 1 -b b --codec protobuf)
123
+ local self_hash_b32libp2pkey=$(echo $self_hash | ipfs cid format -v 1 -b b --codec libp2p-key)
124
+ test_expect_success "resolve of /ipns/{cidv1} with multicodec other than libp2p-key returns a meaningful error" '
125
+ test_expect_code 1 ipfs resolve /ipns/$self_hash_b32protobuf 2>cidcodec_error &&
126
+ grep "Error: peer ID represented as CIDv1 require libp2p-key multicodec: retry with /ipns/$self_hash_b32libp2pkey" cidcodec_error
127
+ '
128
}
129
130
test/sharness/t0184-http-proxy-over-p2p.sh
+34
-1
@@ -144,10 +144,19 @@ test_expect_success 'configure nodes' '
144
iptb testbed create -type localipfs -count 2 -force -init &&
145
ipfsi 0 config --json Experimental.Libp2pStreamMounting true &&
146
ipfsi 1 config --json Experimental.Libp2pStreamMounting true &&
147
- ipfsi 0 config --json Experimental.P2pHttpProxy true
147
+ ipfsi 0 config --json Experimental.P2pHttpProxy true &&
148
ipfsi 0 config --json Addresses.Gateway "[\"/ip4/127.0.0.1/tcp/$IPFS_GATEWAY_PORT\"]"
149
'
150
151
+test_expect_success 'configure a subdomain gateway with /p2p/ path whitelisted' "
152
+ ipfsi 0 config --json Gateway.PublicGateways '{
153
+ \"example.com\": {
154
+ \"UseSubdomains\": true,
155
+ \"Paths\": [\"/p2p/\"]
156
+ }
157
+ }'
158
+"
159
+
160
test_expect_success 'start and connect nodes' '
161
iptb start -wait && iptb connect 0 1
162
'
@@ -206,6 +215,30 @@ test_expect_success 'handle multipart/form-data http request' '
215
curl_send_multipart_form_request 200
216
'
217
218
+# subdomain gateway at *.p2p.example.com requires PeerdID in base32
219
+RECEIVER_ID_CIDv1=$( ipfs cid format -v 1 -b b --codec libp2p-key -- $RECEIVER_ID)
220
+
221
+# OK: $peerid.p2p.example.com/http/index.txt
222
+test_expect_success "handle http request to a subdomain gateway" '
223
+ serve_content "SUBDOMAIN PROVIDES ORIGIN ISOLATION PER RECEIVER_ID" &&
224
+ curl -H "Host: $RECEIVER_ID_CIDv1.p2p.example.com" -sD - $SENDER_GATEWAY/http/index.txt > p2p_response &&
225
+ test_should_contain "SUBDOMAIN PROVIDES ORIGIN ISOLATION PER RECEIVER_ID" p2p_response
226
+'
227
+
228
+# FAIL: $peerid.p2p.example.com/p2p/$peerid/http/index.txt
229
+test_expect_success "handle invalid http request to a subdomain gateway" '
230
+ serve_content "SUBDOMAIN DOES NOT SUPPORT FULL /p2p/ PATH" &&
231
+ curl -H "Host: $RECEIVER_ID_CIDv1.p2p.example.com" -sD - $SENDER_GATEWAY/p2p/$RECEIVER_ID/http/index.txt > p2p_response &&
232
+ test_should_contain "400 Bad Request" p2p_response
233
+'
234
+
235
+# REDIRECT: example.com/p2p/$peerid/http/index.txt → $peerid.p2p.example.com/http/index.txt
236
+test_expect_success "redirect http path request to subdomain gateway" '
237
+ serve_content "SUBDOMAIN ROOT REDIRECTS /p2p/ PATH TO SUBDOMAIN" &&
238
+ curl -H "Host: example.com" -sD - $SENDER_GATEWAY/p2p/$RECEIVER_ID/http/index.txt > p2p_response &&
239
+ test_should_contain "Location: http://$RECEIVER_ID_CIDv1.p2p.example.com/http/index.txt" p2p_response
240
+'
241
+
242
test_expect_success 'stop http server' '
243
teardown_remote_server
244
'