@cryptotaxi247 / kubo / commits / 3ecccd6e1

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? #&lt;&#39;/") {
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? #&lt;&#39;/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 '