diff --git a/cmd/ipfs/daemon.go b/cmd/ipfs/daemon.go index d4647d2f54e..b8486f8aba1 100644 --- a/cmd/ipfs/daemon.go +++ b/cmd/ipfs/daemon.go @@ -12,6 +12,8 @@ import ( "sort" "sync" + multierror "github.com/hashicorp/go-multierror" + version "github.com/ipfs/go-ipfs" config "github.com/ipfs/go-ipfs-config" cserial "github.com/ipfs/go-ipfs-config/serialize" @@ -27,7 +29,6 @@ import ( migrate "github.com/ipfs/go-ipfs/repo/fsrepo/migrations" sockets "github.com/libp2p/go-socket-activation" - "github.com/hashicorp/go-multierror" cmds "github.com/ipfs/go-ipfs-cmds" mprome "github.com/ipfs/go-metrics-prometheus" goprocess "github.com/jbenet/goprocess" @@ -298,9 +299,9 @@ func daemonFunc(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment // Start assembling node config ncfg := &core.BuildCfg{ - Repo: repo, - Permanent: true, // It is temporary way to signify that node is permanent - Online: !offline, + Repo: repo, + Permanent: true, // It is temporary way to signify that node is permanent + Online: !offline, DisableEncryptedConnections: unencrypted, ExtraOpts: map[string]bool{ "pubsub": pubsub, @@ -636,7 +637,7 @@ func serveHTTPGateway(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, e var opts = []corehttp.ServeOption{ corehttp.MetricsCollectionOption("gateway"), - corehttp.IPNSHostnameOption(), + corehttp.HostnameOption(), corehttp.GatewayOption(writable, "/ipfs", "/ipns"), corehttp.VersionOption(), corehttp.CheckVersionOption(), @@ -644,7 +645,7 @@ func serveHTTPGateway(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, e } if cfg.Experimental.P2pHttpProxy { - opts = append(opts, corehttp.ProxyOption()) + opts = append(opts, corehttp.P2PProxyOption()) } if len(cfg.Gateway.RootRedirect) > 0 { diff --git a/core/corehttp/corehttp.go b/core/corehttp/corehttp.go index c52bea8f5c7..d99a0769119 100644 --- a/core/corehttp/corehttp.go +++ b/core/corehttp/corehttp.go @@ -43,7 +43,17 @@ func makeHandler(n *core.IpfsNode, l net.Listener, options ...ServeOption) (http return nil, err } } - return topMux, nil + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // ServeMux does not support requests with CONNECT method, + // so we need to handle them separately + // https://golang.org/src/net/http/request.go#L111 + if r.Method == http.MethodConnect { + w.WriteHeader(http.StatusOK) + return + } + topMux.ServeHTTP(w, r) + }) + return handler, nil } // ListenAndServe runs an HTTP server listening at |listeningMultiAddr| with @@ -70,6 +80,8 @@ func ListenAndServe(n *core.IpfsNode, listeningMultiAddr string, options ...Serv return Serve(n, manet.NetListener(list), options...) } +// Serve accepts incoming HTTP connections on the listener and pass them +// to ServeOption handlers. func Serve(node *core.IpfsNode, lis net.Listener, options ...ServeOption) error { // make sure we close this no matter what. defer lis.Close() diff --git a/core/corehttp/gateway_handler.go b/core/corehttp/gateway_handler.go index de8038f5306..d3c4d26392d 100644 --- a/core/corehttp/gateway_handler.go +++ b/core/corehttp/gateway_handler.go @@ -14,17 +14,16 @@ import ( "strings" "time" - "github.com/dustin/go-humanize" + humanize "github.com/dustin/go-humanize" "github.com/ipfs/go-cid" files "github.com/ipfs/go-ipfs-files" dag "github.com/ipfs/go-merkledag" - "github.com/ipfs/go-mfs" - "github.com/ipfs/go-path" + mfs "github.com/ipfs/go-mfs" + path "github.com/ipfs/go-path" "github.com/ipfs/go-path/resolver" coreiface "github.com/ipfs/interface-go-ipfs-core" ipath "github.com/ipfs/interface-go-ipfs-core/path" routing "github.com/libp2p/go-libp2p-core/routing" - "github.com/multiformats/go-multibase" ) const ( @@ -39,6 +38,25 @@ type gatewayHandler struct { api coreiface.CoreAPI } +// StatusResponseWriter enables us to override HTTP Status Code passed to +// WriteHeader function inside of http.ServeContent. Decision is based on +// presence of HTTP Headers such as Location. +type statusResponseWriter struct { + http.ResponseWriter +} + +func (sw *statusResponseWriter) WriteHeader(code int) { + // Check if we need to adjust Status Code to account for scheduled redirect + // This enables us to return payload along with HTTP 301 + // for subdomain redirect in web browsers while also returning body for cli + // tools which do not follow redirects by default (curl, wget). + redirect := sw.ResponseWriter.Header().Get("Location") + if redirect != "" && code == http.StatusOK { + code = http.StatusMovedPermanently + } + sw.ResponseWriter.WriteHeader(code) +} + func newGatewayHandler(c GatewayConfig, api coreiface.CoreAPI) *gatewayHandler { i := &gatewayHandler{ config: c, @@ -143,17 +161,17 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request } } - // IPNSHostnameOption might have constructed an IPNS path using the Host header. + // HostnameOption might have constructed an IPNS/IPFS path using the Host header. // In this case, we need the original path for constructing redirects // and links that match the requested URL. // For example, http://example.net would become /ipns/example.net, and // the redirects and links would end up as http://example.net/ipns/example.net - originalUrlPath := prefix + urlPath - ipnsHostname := false - if hdr := r.Header.Get("X-Ipns-Original-Path"); len(hdr) > 0 { - originalUrlPath = prefix + hdr - ipnsHostname = true + requestURI, err := url.ParseRequestURI(r.RequestURI) + if err != nil { + webError(w, "failed to parse request path", err, http.StatusInternalServerError) + return } + originalUrlPath := prefix + requestURI.Path // Service Worker registration request if r.Header.Get("Service-Worker") == "script" { @@ -206,39 +224,6 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request w.Header().Set("X-IPFS-Path", urlPath) w.Header().Set("Etag", etag) - // Suborigin header, sandboxes apps from each other in the browser (even - // though they are served from the same gateway domain). - // - // Omitted if the path was treated by IPNSHostnameOption(), for example - // a request for http://example.net/ would be changed to /ipns/example.net/, - // which would turn into an incorrect Suborigin header. - // In this case the correct thing to do is omit the header because it is already - // handled correctly without a Suborigin. - // - // NOTE: This is not yet widely supported by browsers. - if !ipnsHostname { - // e.g.: 1="ipfs", 2="QmYuNaKwY...", ... - pathComponents := strings.SplitN(urlPath, "/", 4) - - var suboriginRaw []byte - cidDecoded, err := cid.Decode(pathComponents[2]) - if err != nil { - // component 2 doesn't decode with cid, so it must be a hostname - suboriginRaw = []byte(strings.ToLower(pathComponents[2])) - } else { - suboriginRaw = cidDecoded.Bytes() - } - - base32Encoded, err := multibase.Encode(multibase.Base32, suboriginRaw) - if err != nil { - internalWebError(w, err) - return - } - - suborigin := pathComponents[1] + "000" + strings.ToLower(base32Encoded) - w.Header().Set("Suborigin", suborigin) - } - // set these headers _after_ the error, for we may just not have it // and dont want the client to cache a 500 response... // and only if it's /ipfs! @@ -322,10 +307,10 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request // construct the correct back link // https://github.com/ipfs/go-ipfs/issues/1365 - var backLink string = prefix + urlPath + var backLink string = originalUrlPath // don't go further up than /ipfs/$hash/ - pathSplit := path.SplitList(backLink) + pathSplit := path.SplitList(urlPath) switch { // keep backlink case len(pathSplit) == 3: // url: /ipfs/$hash @@ -342,18 +327,8 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request } } - // strip /ipfs/$hash from backlink if IPNSHostnameOption touched the path. - if ipnsHostname { - backLink = prefix + "/" - if len(pathSplit) > 5 { - // also strip the trailing segment, because it's a backlink - backLinkParts := pathSplit[3 : len(pathSplit)-2] - backLink += path.Join(backLinkParts) + "/" - } - } - var hash string - if !strings.HasPrefix(originalUrlPath, ipfsPathPrefix) { + if !strings.HasPrefix(urlPath, ipfsPathPrefix) { hash = resolvedPath.Cid().String() } @@ -410,6 +385,7 @@ func (i *gatewayHandler) serveFile(w http.ResponseWriter, req *http.Request, nam } w.Header().Set("Content-Type", ctype) + w = &statusResponseWriter{w} http.ServeContent(w, req, name, modtime, content) } diff --git a/core/corehttp/gateway_test.go b/core/corehttp/gateway_test.go index 9128aa0175d..daf1af07c6c 100644 --- a/core/corehttp/gateway_test.go +++ b/core/corehttp/gateway_test.go @@ -138,7 +138,7 @@ func newTestServerAndNode(t *testing.T, ns mockNamesys) (*httptest.Server, iface dh.Handler, err = makeHandler(n, ts.Listener, - IPNSHostnameOption(), + HostnameOption(), GatewayOption(false, "/ipfs", "/ipns"), VersionOption(), ) @@ -184,12 +184,12 @@ func TestGatewayGet(t *testing.T) { status int text string }{ - {"localhost:5001", "/", http.StatusNotFound, "404 page not found\n"}, - {"localhost:5001", "/" + k.Cid().String(), http.StatusNotFound, "404 page not found\n"}, - {"localhost:5001", k.String(), http.StatusOK, "fnord"}, - {"localhost:5001", "/ipns/nxdomain.example.com", http.StatusNotFound, "ipfs resolve -r /ipns/nxdomain.example.com: " + namesys.ErrResolveFailed.Error() + "\n"}, - {"localhost:5001", "/ipns/%0D%0A%0D%0Ahello", http.StatusNotFound, "ipfs resolve -r /ipns/%0D%0A%0D%0Ahello: " + namesys.ErrResolveFailed.Error() + "\n"}, - {"localhost:5001", "/ipns/example.com", http.StatusOK, "fnord"}, + {"127.0.0.1:8080", "/", http.StatusNotFound, "404 page not found\n"}, + {"127.0.0.1:8080", "/" + k.Cid().String(), http.StatusNotFound, "404 page not found\n"}, + {"127.0.0.1:8080", k.String(), http.StatusOK, "fnord"}, + {"127.0.0.1:8080", "/ipns/nxdomain.example.com", http.StatusNotFound, "ipfs resolve -r /ipns/nxdomain.example.com: " + namesys.ErrResolveFailed.Error() + "\n"}, + {"127.0.0.1:8080", "/ipns/%0D%0A%0D%0Ahello", http.StatusNotFound, "ipfs resolve -r /ipns/%0D%0A%0D%0Ahello: " + namesys.ErrResolveFailed.Error() + "\n"}, + {"127.0.0.1:8080", "/ipns/example.com", http.StatusOK, "fnord"}, {"example.com", "/", http.StatusOK, "fnord"}, {"working.example.com", "/", http.StatusOK, "fnord"}, @@ -381,7 +381,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) { if !strings.Contains(s, "Index of /foo? #<'/") { t.Fatalf("expected a path in directory listing") } - if !strings.Contains(s, "") { + if !strings.Contains(s, "") { t.Fatalf("expected backlink in directory listing") } if !strings.Contains(s, "") { @@ -447,7 +447,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) { if !strings.Contains(s, "Index of /foo? #<'/bar/") { t.Fatalf("expected a path in directory listing") } - if !strings.Contains(s, "") { + if !strings.Contains(s, "") { t.Fatalf("expected backlink in directory listing") } if !strings.Contains(s, "") { diff --git a/core/corehttp/hostname.go b/core/corehttp/hostname.go new file mode 100644 index 00000000000..143435106e5 --- /dev/null +++ b/core/corehttp/hostname.go @@ -0,0 +1,374 @@ +package corehttp + +import ( + "context" + "fmt" + "net" + "net/http" + "net/url" + "strings" + + cid "github.com/ipfs/go-cid" + core "github.com/ipfs/go-ipfs/core" + coreapi "github.com/ipfs/go-ipfs/core/coreapi" + namesys "github.com/ipfs/go-ipfs/namesys" + isd "github.com/jbenet/go-is-domain" + "github.com/libp2p/go-libp2p-core/peer" + mbase "github.com/multiformats/go-multibase" + + config "github.com/ipfs/go-ipfs-config" + iface "github.com/ipfs/interface-go-ipfs-core" + options "github.com/ipfs/interface-go-ipfs-core/options" + nsopts "github.com/ipfs/interface-go-ipfs-core/options/namesys" +) + +var defaultPaths = []string{"/ipfs/", "/ipns/", "/api/", "/p2p/", "/version"} + +var pathGatewaySpec = config.GatewaySpec{ + Paths: defaultPaths, + UseSubdomains: false, +} + +var subdomainGatewaySpec = config.GatewaySpec{ + Paths: defaultPaths, + UseSubdomains: true, +} + +var defaultKnownGateways = map[string]config.GatewaySpec{ + "localhost": subdomainGatewaySpec, + "ipfs.io": pathGatewaySpec, + "gateway.ipfs.io": pathGatewaySpec, + "dweb.link": subdomainGatewaySpec, +} + +// HostnameOption rewrites an incoming request based on the Host header. +func HostnameOption() ServeOption { + return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) { + childMux := http.NewServeMux() + + coreApi, err := coreapi.NewCoreAPI(n) + if err != nil { + return nil, err + } + + cfg, err := n.Repo.Config() + if err != nil { + return nil, err + } + knownGateways := make( + map[string]config.GatewaySpec, + len(defaultKnownGateways)+len(cfg.Gateway.PublicGateways), + ) + for hostname, gw := range defaultKnownGateways { + knownGateways[hostname] = gw + } + for hostname, gw := range cfg.Gateway.PublicGateways { + if gw == nil { + // Allows the user to remove gateways but _also_ + // allows us to continuously update the list. + delete(knownGateways, hostname) + } else { + knownGateways[hostname] = *gw + } + } + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Unfortunately, many (well, ipfs.io) gateways use + // DNSLink so if we blindly rewrite with DNSLink, we'll + // break /ipfs links. + // + // We fix this by maintaining a list of known gateways + // and the paths that they serve "gateway" content on. + // That way, we can use DNSLink for everything else. + + // HTTP Host & Path check: is this one of our "known gateways"? + if gw, ok := isKnownHostname(r.Host, knownGateways); ok { + // This is a known gateway but request is not using + // the subdomain feature. + + // Does this gateway _handle_ this path? + if hasPrefix(r.URL.Path, gw.Paths...) { + // It does. + + // Should this gateway use subdomains instead of paths? + if gw.UseSubdomains { + // Yes, redirect if applicable + // Example: dweb.link/ipfs/{cid} → {cid}.ipfs.dweb.link + if newURL, ok := toSubdomainURL(r.Host, r.URL.Path, r); ok { + // Just to be sure single Origin can't be abused in + // web browsers that ignored the redirect for some + // reason, Clear-Site-Data header clears browsing + // data (cookies, storage etc) associated with + // hostname's root Origin + // Note: we can't use "*" due to bug in Chromium: + // https://bugs.chromium.org/p/chromium/issues/detail?id=898503 + w.Header().Set("Clear-Site-Data", "\"cookies\", \"storage\"") + + // Set "Location" header with redirect destination. + // It is ignored by curl in default mode, but will + // be respected by user agents that follow + // redirects by default, namely web browsers + w.Header().Set("Location", newURL) + + // Note: we continue regular gateway processing: + // HTTP Status Code http.StatusMovedPermanently + // will be set later, in statusResponseWriter + } + } + + // Not a subdomain resource, continue with path processing + // Example: 127.0.0.1:8080/ipfs/{CID}, ipfs.io/ipfs/{CID} etc + childMux.ServeHTTP(w, r) + return + } + // Not a whitelisted path + + // Try DNSLink, if it was not explicitly disabled for the hostname + if !gw.NoDNSLink && isDNSLinkRequest(n.Context(), coreApi, r) { + // rewrite path and handle as DNSLink + r.URL.Path = "/ipns/" + stripPort(r.Host) + r.URL.Path + childMux.ServeHTTP(w, r) + return + } + + // If not, resource does not exist on the hostname, return 404 + http.NotFound(w, r) + return + } + + // HTTP Host check: is this one of our subdomain-based "known gateways"? + // Example: {cid}.ipfs.localhost, {cid}.ipfs.dweb.link + if gw, hostname, ns, rootID, ok := knownSubdomainDetails(r.Host, knownGateways); ok { + // Looks like we're using known subdomain gateway. + + // Assemble original path prefix. + pathPrefix := "/" + ns + "/" + rootID + + // Does this gateway _handle_ this path? + if !(gw.UseSubdomains && hasPrefix(pathPrefix, gw.Paths...)) { + // If not, resource does not exist, return 404 + http.NotFound(w, r) + return + } + + // Do we need to fix multicodec in PeerID represented as CIDv1? + if isPeerIDNamespace(ns) { + keyCid, err := cid.Decode(rootID) + if err == nil && keyCid.Type() != cid.Libp2pKey { + if newURL, ok := toSubdomainURL(hostname, pathPrefix+r.URL.Path, r); ok { + // Redirect to CID fixed inside of toSubdomainURL() + http.Redirect(w, r, newURL, http.StatusMovedPermanently) + return + } + } + } + + // Rewrite the path to not use subdomains + r.URL.Path = pathPrefix + r.URL.Path + + // Serve path request + childMux.ServeHTTP(w, r) + return + } + // We don't have a known gateway. Fallback on DNSLink lookup + + // Wildcard HTTP Host check: + // 1. is wildcard DNSLink enabled (Gateway.NoDNSLink=false)? + // 2. does Host header include a fully qualified domain name (FQDN)? + // 3. does DNSLink record exist in DNS? + if !cfg.Gateway.NoDNSLink && isDNSLinkRequest(n.Context(), coreApi, r) { + // rewrite path and handle as DNSLink + r.URL.Path = "/ipns/" + stripPort(r.Host) + r.URL.Path + childMux.ServeHTTP(w, r) + return + } + + // else, treat it as an old school gateway, I guess. + childMux.ServeHTTP(w, r) + }) + return childMux, nil + } +} + +// isKnownHostname checks Gateway.PublicGateways and returns matching +// GatewaySpec with gracefull fallback to version without port +func isKnownHostname(hostname string, knownGateways map[string]config.GatewaySpec) (gw config.GatewaySpec, ok bool) { + // Try hostname (host+optional port - value from Host header as-is) + if gw, ok := knownGateways[hostname]; ok { + return gw, ok + } + // Fallback to hostname without port + gw, ok = knownGateways[stripPort(hostname)] + return gw, ok +} + +// Parses Host header and looks for a known subdomain gateway host. +// If found, returns GatewaySpec and subdomain components. +// Note: hostname is host + optional port +func knownSubdomainDetails(hostname string, knownGateways map[string]config.GatewaySpec) (gw config.GatewaySpec, knownHostname, ns, rootID string, ok bool) { + labels := strings.Split(hostname, ".") + // Look for FQDN of a known gateway hostname. + // Example: given "dist.ipfs.io.ipns.dweb.link": + // 1. Lookup "link" TLD in knownGateways: negative + // 2. Lookup "dweb.link" in knownGateways: positive + // + // Stops when we have 2 or fewer labels left as we need at least a + // rootId and a namespace. + for i := len(labels) - 1; i >= 2; i-- { + fqdn := strings.Join(labels[i:], ".") + gw, ok := isKnownHostname(fqdn, knownGateways) + if !ok { + continue + } + + ns := labels[i-1] + if !isSubdomainNamespace(ns) { + break + } + + // Merge remaining labels (could be a FQDN with DNSLink) + rootID := strings.Join(labels[:i-1], ".") + return gw, fqdn, ns, rootID, true + } + // not a known subdomain gateway + return gw, "", "", "", false +} + +// isDNSLinkRequest returns bool that indicates if request +// should return data from content path listed in DNSLink record (if exists) +func isDNSLinkRequest(ctx context.Context, ipfs iface.CoreAPI, r *http.Request) bool { + fqdn := stripPort(r.Host) + if len(fqdn) == 0 && !isd.IsDomain(fqdn) { + return false + } + name := "/ipns/" + fqdn + // check if DNSLink exists + depth := options.Name.ResolveOption(nsopts.Depth(1)) + _, err := ipfs.Name().Resolve(ctx, name, depth) + return err == nil || err == namesys.ErrResolveRecursion +} + +func isSubdomainNamespace(ns string) bool { + switch ns { + case "ipfs", "ipns", "p2p", "ipld": + return true + default: + return false + } +} + +func isPeerIDNamespace(ns string) bool { + switch ns { + case "ipns", "p2p": + return true + default: + return false + } +} + +// Converts a hostname/path to a subdomain-based URL, if applicable. +func toSubdomainURL(hostname, path string, r *http.Request) (redirURL string, ok bool) { + var scheme, ns, rootID, rest string + + query := r.URL.RawQuery + parts := strings.SplitN(path, "/", 4) + safeRedirectURL := func(in string) (out string, ok bool) { + safeURI, err := url.ParseRequestURI(in) + if err != nil { + return "", false + } + return safeURI.String(), true + } + + // Support X-Forwarded-Proto if added by a reverse proxy + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto + xproto := r.Header.Get("X-Forwarded-Proto") + if xproto == "https" { + scheme = "https:" + } else { + scheme = "http:" + } + + switch len(parts) { + case 4: + rest = parts[3] + fallthrough + case 3: + ns = parts[1] + rootID = parts[2] + default: + return "", false + } + + if !isSubdomainNamespace(ns) { + return "", false + } + + // add prefix if query is present + if query != "" { + query = "?" + query + } + + // Normalize problematic PeerIDs (eg. ed25519+identity) to CID representation + if isPeerIDNamespace(ns) && !isd.IsDomain(rootID) { + peerID, err := peer.Decode(rootID) + // Note: PeerID CIDv1 with protobuf multicodec will fail, but we fix it + // in the next block + if err == nil { + rootID = peer.ToCid(peerID).String() + } + } + + // If rootID is a CID, ensure it uses DNS-friendly text representation + if rootCid, err := cid.Decode(rootID); err == nil { + multicodec := rootCid.Type() + + // PeerIDs represented as CIDv1 are expected to have libp2p-key + // multicodec (https://github.com/libp2p/specs/pull/209). + // We ease the transition by fixing multicodec on the fly: + // https://github.com/ipfs/go-ipfs/issues/5287#issuecomment-492163929 + if isPeerIDNamespace(ns) && multicodec != cid.Libp2pKey { + multicodec = cid.Libp2pKey + } + + // if object turns out to be a valid CID, + // ensure text representation used in subdomain is CIDv1 in Base32 + // https://github.com/ipfs/in-web-browsers/issues/89 + rootID, err = cid.NewCidV1(multicodec, rootCid.Hash()).StringOfBase(mbase.Base32) + if err != nil { + // should not error, but if it does, its clealy not possible to + // produce a subdomain URL + return "", false + } + } + + return safeRedirectURL(fmt.Sprintf( + "%s//%s.%s.%s/%s%s", + scheme, + rootID, + ns, + hostname, + rest, + query, + )) +} + +func hasPrefix(path string, prefixes ...string) bool { + for _, prefix := range prefixes { + // Assume people are creative with trailing slashes in Gateway config + p := strings.TrimSuffix(prefix, "/") + // Support for both /version and /ipfs/$cid + if p == path || strings.HasPrefix(path, p+"/") { + return true + } + } + return false +} + +func stripPort(hostname string) string { + host, _, err := net.SplitHostPort(hostname) + if err == nil { + return host + } + return hostname +} diff --git a/core/corehttp/hostname_test.go b/core/corehttp/hostname_test.go new file mode 100644 index 00000000000..9a297464891 --- /dev/null +++ b/core/corehttp/hostname_test.go @@ -0,0 +1,152 @@ +package corehttp + +import ( + "net/http/httptest" + "testing" + + config "github.com/ipfs/go-ipfs-config" +) + +func TestToSubdomainURL(t *testing.T) { + r := httptest.NewRequest("GET", "http://request-stub.example.com", nil) + for _, test := range []struct { + // in: + hostname string + path string + // out: + url string + ok bool + }{ + // DNSLink + {"localhost", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", true}, + // Hostname with port + {"localhost:8080", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", true}, + // CIDv0 → CIDv1base32 + {"localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", true}, + // PeerID as CIDv1 needs to have libp2p-key multicodec + {"localhost", "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://bafzbeieqhtl2l3mrszjnhv6hf2iloiitsx7mexiolcnywnbcrzkqxwslja.ipns.localhost/", true}, + {"localhost", "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://bafzbeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm.ipns.localhost/", true}, + // PeerID: ed25519+identity multihash + {"localhost", "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://bafzaajaiaejcat4yhiwnr2qz73mtu6vrnj2krxlpfoa3wo2pllfi37quorgwh2jw.ipns.localhost/", true}, + } { + url, ok := toSubdomainURL(test.hostname, test.path, r) + if ok != test.ok || url != test.url { + t.Errorf("(%s, %s) returned (%s, %t), expected (%s, %t)", test.hostname, test.path, url, ok, test.url, ok) + } + } +} + +func TestHasPrefix(t *testing.T) { + for _, test := range []struct { + prefixes []string + path string + out bool + }{ + {[]string{"/ipfs"}, "/ipfs/cid", true}, + {[]string{"/ipfs/"}, "/ipfs/cid", true}, + {[]string{"/version/"}, "/version", true}, + {[]string{"/version"}, "/version", true}, + } { + out := hasPrefix(test.path, test.prefixes...) + if out != test.out { + t.Errorf("(%+v, %s) returned '%t', expected '%t'", test.prefixes, test.path, out, test.out) + } + } +} + +func TestPortStripping(t *testing.T) { + for _, test := range []struct { + in string + out string + }{ + {"localhost:8080", "localhost"}, + {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.localhost:8080", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.localhost"}, + {"example.com:443", "example.com"}, + {"example.com", "example.com"}, + {"foo-dweb.ipfs.pvt.k12.ma.us:8080", "foo-dweb.ipfs.pvt.k12.ma.us"}, + {"localhost", "localhost"}, + {"[::1]:8080", "::1"}, + } { + out := stripPort(test.in) + if out != test.out { + t.Errorf("(%s): returned '%s', expected '%s'", test.in, out, test.out) + } + } + +} + +func TestKnownSubdomainDetails(t *testing.T) { + gwSpec := config.GatewaySpec{ + UseSubdomains: true, + } + knownGateways := map[string]config.GatewaySpec{ + "localhost": gwSpec, + "dweb.link": gwSpec, + "dweb.ipfs.pvt.k12.ma.us": gwSpec, // note the sneaky ".ipfs." ;-) + } + + for _, test := range []struct { + // in: + hostHeader string + // out: + hostname string + ns string + rootID string + ok bool + }{ + // no subdomain + {"127.0.0.1:8080", "", "", "", false}, + {"[::1]:8080", "", "", "", false}, + {"hey.look.example.com", "", "", "", false}, + {"dweb.link", "", "", "", false}, + // malformed Host header + {".....dweb.link", "", "", "", false}, + {"link", "", "", "", false}, + {"8080:dweb.link", "", "", "", false}, + {" ", "", "", "", false}, + {"", "", "", "", false}, + // unknown gateway host + {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.unknown.example.com", "", "", "", false}, + // cid in subdomain, known gateway + {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.localhost:8080", "localhost:8080", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true}, + {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.dweb.link", "dweb.link", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true}, + // capture everything before .ipfs. + {"foo.bar.boo-buzz.ipfs.dweb.link", "dweb.link", "ipfs", "foo.bar.boo-buzz", true}, + // ipns + {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.localhost:8080", "localhost:8080", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true}, + {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.dweb.link", "dweb.link", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true}, + // edge case check: public gateway under long TLD (see: https://publicsuffix.org) + {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.dweb.ipfs.pvt.k12.ma.us", "dweb.ipfs.pvt.k12.ma.us", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true}, + {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.dweb.ipfs.pvt.k12.ma.us", "dweb.ipfs.pvt.k12.ma.us", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true}, + // dnslink in subdomain + {"en.wikipedia-on-ipfs.org.ipns.localhost:8080", "localhost:8080", "ipns", "en.wikipedia-on-ipfs.org", true}, + {"en.wikipedia-on-ipfs.org.ipns.localhost", "localhost", "ipns", "en.wikipedia-on-ipfs.org", true}, + {"dist.ipfs.io.ipns.localhost:8080", "localhost:8080", "ipns", "dist.ipfs.io", true}, + {"en.wikipedia-on-ipfs.org.ipns.dweb.link", "dweb.link", "ipns", "en.wikipedia-on-ipfs.org", true}, + // edge case check: public gateway under long TLD (see: https://publicsuffix.org) + {"foo.dweb.ipfs.pvt.k12.ma.us", "", "", "", false}, + {"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.dweb.ipfs.pvt.k12.ma.us", "dweb.ipfs.pvt.k12.ma.us", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true}, + {"bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju.ipns.dweb.ipfs.pvt.k12.ma.us", "dweb.ipfs.pvt.k12.ma.us", "ipns", "bafzbeihe35nmjqar22thmxsnlsgxppd66pseq6tscs4mo25y55juhh6bju", true}, + // other namespaces + {"api.localhost", "", "", "", false}, + {"peerid.p2p.localhost", "localhost", "p2p", "peerid", true}, + } { + gw, hostname, ns, rootID, ok := knownSubdomainDetails(test.hostHeader, knownGateways) + if ok != test.ok { + t.Errorf("knownSubdomainDetails(%s): ok is %t, expected %t", test.hostHeader, ok, test.ok) + } + if rootID != test.rootID { + t.Errorf("knownSubdomainDetails(%s): rootID is '%s', expected '%s'", test.hostHeader, rootID, test.rootID) + } + if ns != test.ns { + t.Errorf("knownSubdomainDetails(%s): ns is '%s', expected '%s'", test.hostHeader, ns, test.ns) + } + if hostname != test.hostname { + t.Errorf("knownSubdomainDetails(%s): hostname is '%s', expected '%s'", test.hostHeader, hostname, test.hostname) + } + if ok && gw.UseSubdomains != gwSpec.UseSubdomains { + t.Errorf("knownSubdomainDetails(%s): gw is %+v, expected %+v", test.hostHeader, gw, gwSpec) + } + } + +} diff --git a/core/corehttp/ipns_hostname.go b/core/corehttp/ipns_hostname.go deleted file mode 100644 index d5512779b35..00000000000 --- a/core/corehttp/ipns_hostname.go +++ /dev/null @@ -1,39 +0,0 @@ -package corehttp - -import ( - "context" - "net" - "net/http" - "strings" - - core "github.com/ipfs/go-ipfs/core" - namesys "github.com/ipfs/go-ipfs/namesys" - - nsopts "github.com/ipfs/interface-go-ipfs-core/options/namesys" - isd "github.com/jbenet/go-is-domain" -) - -// IPNSHostnameOption rewrites an incoming request if its Host: header contains -// an IPNS name. -// The rewritten request points at the resolved name on the gateway handler. -func IPNSHostnameOption() ServeOption { - return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) { - childMux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - ctx, cancel := context.WithCancel(n.Context()) - defer cancel() - - host := strings.SplitN(r.Host, ":", 2)[0] - if len(host) > 0 && isd.IsDomain(host) { - name := "/ipns/" + host - _, err := n.Namesys.Resolve(ctx, name, nsopts.Depth(1)) - if err == nil || err == namesys.ErrResolveRecursion { - r.Header.Set("X-Ipns-Original-Path", r.URL.Path) - r.URL.Path = name + r.URL.Path - } - } - childMux.ServeHTTP(w, r) - }) - return childMux, nil - } -} diff --git a/core/corehttp/proxy.go b/core/corehttp/p2p_proxy.go similarity index 94% rename from core/corehttp/proxy.go rename to core/corehttp/p2p_proxy.go index 17cb0052824..0a615c33a2a 100644 --- a/core/corehttp/proxy.go +++ b/core/corehttp/p2p_proxy.go @@ -14,8 +14,8 @@ import ( p2phttp "github.com/libp2p/go-libp2p-http" ) -// ProxyOption is an endpoint for proxying a HTTP request to another ipfs peer -func ProxyOption() ServeOption { +// P2PProxyOption is an endpoint for proxying a HTTP request to another ipfs peer +func P2PProxyOption() ServeOption { return func(ipfsNode *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) { mux.HandleFunc("/p2p/", func(w http.ResponseWriter, request *http.Request) { // parse request diff --git a/core/corehttp/proxy_test.go b/core/corehttp/p2p_proxy_test.go similarity index 100% rename from core/corehttp/proxy_test.go rename to core/corehttp/p2p_proxy_test.go diff --git a/docs/config.md b/docs/config.md index 5a3446b9dd9..9efe56c5359 100644 --- a/docs/config.md +++ b/docs/config.md @@ -83,10 +83,13 @@ Available profiles: - [`Routing.Type`](#routingtype) - [`Gateway`](#gateway) - [`Gateway.NoFetch`](#gatewaynofetch) + - [`Gateway.NoDNSLink`](#gatewaynodnslink) - [`Gateway.HTTPHeaders`](#gatewayhttpheaders) - [`Gateway.RootRedirect`](#gatewayrootredirect) - [`Gateway.Writable`](#gatewaywritable) - [`Gateway.PathPrefixes`](#gatewaypathprefixes) + - [`Gateway.PublicGateways`](#gatewaypublicgateways) + - [`Gateway` recipes](#gateway-recipes) - [`Identity`](#identity) - [`Identity.PeerID`](#identitypeerid) - [`Identity.PrivKey`](#identityprivkey) @@ -348,6 +351,14 @@ and will not fetch files from the network. Default: `false` +### `Gateway.NoDNSLink` + +A boolean to configure whether DNSLink lookup for value in `Host` HTTP header +should be performed. If DNSLink is present, content path stored in the DNS TXT +record becomes the `/` and respective payload is returned to the client. + +Default: `false` + ### `Gateway.HTTPHeaders` Headers to set on gateway responses. @@ -379,7 +390,6 @@ A boolean to configure whether the gateway is writeable or not. Default: `false` - ### `Gateway.PathPrefixes` Array of acceptable url paths that a client can specify in X-Ipfs-Path-Prefix @@ -409,6 +419,145 @@ location /blog/ { Default: `[]` + +### `Gateway.PublicGateways` + +`PublicGateways` is a dictionary for defining gateway behavior on specified hostnames. + +#### `Gateway.PublicGateways: Paths` + +Array of paths that should be exposed on the hostname. + +Example: +```json +{ + "Gateway": { + "PublicGateways": { + "example.com": { + "Paths": ["/ipfs", "/ipns"], +``` + +Above enables `http://example.com/ipfs/*` and `http://example.com/ipns/*` but not `http://example.com/api/*` + +Default: `[]` + +#### `Gateway.PublicGateways: UseSubdomains` + +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) +between content roots. + +- `true` - enables [subdomain gateway](#https://docs-beta.ipfs.io/how-to/address-ipfs-on-web/#subdomain-gateway) at `http://*.{hostname}/` + - **Requires whitelist:** make sure respective `Paths` are set. + For example, `Paths: ["/ipfs", "/ipns"]` are required for `http://{cid}.ipfs.{hostname}` and `http://{foo}.ipns.{hostname}` to work: + ```json + { + "Gateway": { + "PublicGateways": { + "dweb.link": { + "UseSubdomains": true, + "Paths": ["/ipfs", "/ipns"], + ``` + - **Backward-compatible:** requests for content paths such as `http://{hostname}/ipfs/{cid}` produce redirect to `http://{cid}.ipfs.{hostname}` + - **API:** if `/api` is on the `Paths` whitelist, `http://{hostname}/api/{cmd}` produces redirect to `http://api.{hostname}/api/{cmd}` + +- `false` - enables [path gateway](https://docs-beta.ipfs.io/how-to/address-ipfs-on-web/#path-gateway) at `http://{hostname}/*` + - Example: + ```json + { + "Gateway": { + "PublicGateways": { + "ipfs.io": { + "UseSubdomains": false, + "Paths": ["/ipfs", "/ipns", "/api"], + ``` + + +Default: `false` + + +#### `Gateway.PublicGateways: NoDNSLink` + +A boolean to configure whether DNSLink for hostname present in `Host` +HTTP header should be resolved. Overrides global setting. +If `Paths` are defined, they take priority over DNSLink. + +Default: `false` (DNSLink lookup enabled by default for every defined hostname) + +#### Implicit defaults of `Gateway.PublicGateways` + +Default entries for `localhost` hostname and loopback IPs are always present. +If additional config is provided for those hostnames, it will be merged on top of implicit values: +```json +{ + "Gateway": { + "PublicGateways": { + "localhost": { + "Paths": ["/ipfs", "/ipns"], + "UseSubdomains": true + } + } + } +} +``` + +It is also possible to remove a default by setting it to `null`. +For example, to disable subdomain gateway on `localhost` +and make that hostname act the same as `127.0.0.1`: + +```console +$ ipfs config --json Gateway.PublicGateways '{"localhost": null }' +``` + +### `Gateway` recipes + +Below is a list of the most common public gateway setups. + +* 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) + ```console + $ ipfs config --json Gateway.PublicGateways '{ + "dweb.link": { + "UseSubdomains": true, + "Paths": ["/ipfs", "/ipns"] + } + }' + ``` + **Note:** this enables automatic redirects from content paths to subdomains + `http://dweb.link/ipfs/{cid}` → `http://{cid}.ipfs.dweb.link` + +* 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) + ```console + $ ipfs config --json Gateway.PublicGateways '{ + "ipfs.io": { + "UseSubdomains": false, + "Paths": ["/ipfs", "/ipns", "/api"] + } + }' + ``` + +* Public [DNSLink](https://dnslink.io/) gateway resolving every hostname passed in `Host` header. + ```console + $ ipfs config --json Gateway.NoDNSLink true + ``` + * Note that `NoDNSLink: false` is the default (it works out of the box unless set to `true` manually) + +* Hardened, site-specific [DNSLink gateway](https://docs-beta.ipfs.io/how-to/address-ipfs-on-web/#dnslink-gateway). + Disable fetching of remote data (`NoFetch: true`) + and resolving DNSLink at unknown hostnames (`NoDNSLink: true`). + Then, enable DNSLink gateway only for the specific hostname (for which data + is already present on the node), without exposing any content-addressing `Paths`: + "NoFetch": true, + "NoDNSLink": true, + ```console + $ ipfs config --json Gateway.NoFetch true + $ ipfs config --json Gateway.NoDNSLink true + $ ipfs config --json Gateway.PublicGateways '{ + "en.wikipedia-on-ipfs.org": { + "NoDNSLink": false, + "Paths": [] + } + }' + ``` + ## `Identity` ### `Identity.PeerID` diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 82eb5c3126c..b64a4e2481d 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -82,6 +82,20 @@ the `--migrate` flag). Default: https://ipfs.io/ipfs/$something (depends on the IPFS version) +## `IPFS_NS_MAP` + +Adds static namesys records for deteministic tests and debugging. +Useful for testing things like DNSLink without real DNS lookup. + +Example: + +```console +$ IPFS_NS_MAP="dnslink-test1.example.com:/ipfs/bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am,dnslink-test2.example.com:/ipns/dnslink-test1.example.com" ipfs daemon +... +$ ipfs resolve -r /ipns/dnslink-test2.example.com +/ipfs/bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am +``` + ## `LIBP2P_MUX_PREFS` Tells go-ipfs which multiplexers to use in which order. diff --git a/go.mod b/go.mod index 53ccb013976..c1823843bd9 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( github.com/ipfs/go-ipfs-blockstore v0.1.4 github.com/ipfs/go-ipfs-chunker v0.0.4 github.com/ipfs/go-ipfs-cmds v0.1.2 - github.com/ipfs/go-ipfs-config v0.2.1 + github.com/ipfs/go-ipfs-config v0.3.0 github.com/ipfs/go-ipfs-ds-help v0.1.1 github.com/ipfs/go-ipfs-exchange-interface v0.0.1 github.com/ipfs/go-ipfs-exchange-offline v0.0.1 diff --git a/go.sum b/go.sum index 88d51c67606..9a8b551dc0b 100644 --- a/go.sum +++ b/go.sum @@ -268,14 +268,10 @@ github.com/ipfs/go-ipfs-chunker v0.0.1 h1:cHUUxKFQ99pozdahi+uSC/3Y6HeRpi9oTeUHbE github.com/ipfs/go-ipfs-chunker v0.0.1/go.mod h1:tWewYK0we3+rMbOh7pPFGDyypCtvGcBFymgY4rSDLAw= github.com/ipfs/go-ipfs-chunker v0.0.4 h1:nb2ZIgtOk0TxJ5KDBEk+sv6iqJTF/PHg6owN2xCrUjE= github.com/ipfs/go-ipfs-chunker v0.0.4/go.mod h1:jhgdF8vxRHycr00k13FM8Y0E+6BoalYeobXmUyTreP8= -github.com/ipfs/go-ipfs-cmds v0.1.1 h1:H9/BLf5rcsULHMj/x8gC0e5o+raYhqk1OQsfzbGMNM4= -github.com/ipfs/go-ipfs-cmds v0.1.1/go.mod h1:k1zMXcOLtljA9iAnZHddbH69yVm5+weRL0snmMD/rK0= -github.com/ipfs/go-ipfs-cmds v0.1.2-0.20200316211807-0c2a21b0dacc h1:HIG2l6XUnov+M6UwcUKKrwGc8Q+n9AYGbiGM4pK21SM= -github.com/ipfs/go-ipfs-cmds v0.1.2-0.20200316211807-0c2a21b0dacc/go.mod h1:a9LyFOtQCnVc3BvbAgW+GrMXEuN29aLCNi3Wk0IM8wo= github.com/ipfs/go-ipfs-cmds v0.1.2 h1:02FLzTA9jYRle/xdMWYwGwxu3gzC3GhPUaz35dH+FrY= github.com/ipfs/go-ipfs-cmds v0.1.2/go.mod h1:a9LyFOtQCnVc3BvbAgW+GrMXEuN29aLCNi3Wk0IM8wo= -github.com/ipfs/go-ipfs-config v0.2.1 h1:Mpyvdf9Zc8k3jg+sRe8e9iylYXHYXqFMuePUjAZQvsE= -github.com/ipfs/go-ipfs-config v0.2.1/go.mod h1:zCKH1uf1XIvf67589BnQ5IAv/Pld2J3gQoQYvG8TK8w= +github.com/ipfs/go-ipfs-config v0.3.0 h1:fGs3JBqB9ia/Joi8up47uiKn150EOEqqVFwv8HZqXao= +github.com/ipfs/go-ipfs-config v0.3.0/go.mod h1:nSLCFtlaL+2rbl3F+9D4gQZQbT1LjRKx7TJg/IHz6oM= github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ= github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= diff --git a/namesys/cache.go b/namesys/cache.go index 4a5cb5113ae..a0029829d33 100644 --- a/namesys/cache.go +++ b/namesys/cache.go @@ -7,6 +7,14 @@ import ( ) func (ns *mpns) cacheGet(name string) (path.Path, bool) { + // existence of optional mapping defined via IPFS_NS_MAP is checked first + if ns.staticMap != nil { + val, ok := ns.staticMap[name] + if ok { + return val, true + } + } + if ns.cache == nil { return "", false } diff --git a/namesys/namesys.go b/namesys/namesys.go index 079eecccc03..a486b83b8a3 100644 --- a/namesys/namesys.go +++ b/namesys/namesys.go @@ -2,10 +2,13 @@ package namesys import ( "context" + "fmt" + "os" "strings" "time" lru "github.com/hashicorp/golang-lru" + cid "github.com/ipfs/go-cid" ds "github.com/ipfs/go-datastore" path "github.com/ipfs/go-path" opts "github.com/ipfs/interface-go-ipfs-core/options/namesys" @@ -13,7 +16,6 @@ import ( ci "github.com/libp2p/go-libp2p-core/crypto" peer "github.com/libp2p/go-libp2p-core/peer" routing "github.com/libp2p/go-libp2p-core/routing" - mh "github.com/multiformats/go-multihash" ) // mpns (a multi-protocol NameSystem) implements generic IPFS naming. @@ -29,25 +31,45 @@ type mpns struct { dnsResolver, proquintResolver, ipnsResolver resolver ipnsPublisher Publisher - cache *lru.Cache + staticMap map[string]path.Path + cache *lru.Cache } // NewNameSystem will construct the IPFS naming system based on Routing func NewNameSystem(r routing.ValueStore, ds ds.Datastore, cachesize int) NameSystem { - var cache *lru.Cache + var ( + cache *lru.Cache + staticMap map[string]path.Path + ) if cachesize > 0 { cache, _ = lru.New(cachesize) } + // Prewarm namesys cache with static records for deteministic tests and debugging. + // Useful for testing things like DNSLink without real DNS lookup. + // Example: + // IPFS_NS_MAP="dnslink-test.example.com:/ipfs/bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am" + if list := os.Getenv("IPFS_NS_MAP"); list != "" { + staticMap = make(map[string]path.Path) + for _, pair := range strings.Split(list, ",") { + mapping := strings.SplitN(pair, ":", 2) + key := mapping[0] + value := path.FromString(mapping[1]) + staticMap[key] = value + } + } + return &mpns{ dnsResolver: NewDNSResolver(), proquintResolver: new(ProquintResolver), ipnsResolver: NewIpnsResolver(r), ipnsPublisher: NewIpnsPublisher(r, ds), + staticMap: staticMap, cache: cache, } } +// DefaultResolverCacheTTL defines max ttl of a record placed in namesys cache. const DefaultResolverCacheTTL = time.Minute // Resolve implements Resolver. @@ -112,12 +134,28 @@ func (ns *mpns) resolveOnceAsync(ctx context.Context, name string, options opts. } // Resolver selection: - // 1. if it is a multihash resolve through "ipns". + // 1. if it is a PeerID/CID/multihash resolve through "ipns". // 2. if it is a domain name, resolve through "dns" // 3. otherwise resolve through the "proquint" resolver var res resolver - if _, err := mh.FromB58String(key); err == nil { + _, err := peer.Decode(key) + + // CIDs in IPNS are expected to have libp2p-key multicodec + // We ease the transition by returning a more meaningful error with a valid CID + if err != nil && err.Error() == "can't convert CID of type protobuf to a peer ID" { + ipnsCid, cidErr := cid.Decode(key) + if cidErr == nil && ipnsCid.Version() == 1 && ipnsCid.Type() != cid.Libp2pKey { + fixedCid := cid.NewCidV1(cid.Libp2pKey, ipnsCid.Hash()).String() + codecErr := fmt.Errorf("peer ID represented as CIDv1 require libp2p-key multicodec: retry with /ipns/%s", fixedCid) + log.Debugf("RoutingResolver: could not convert public key hash %s to peer ID: %s\n", key, codecErr) + out <- onceResult{err: codecErr} + close(out) + return out + } + } + + if err == nil { res = ns.ipnsResolver } else if isd.IsDomain(key) { res = ns.dnsResolver diff --git a/namesys/namesys_test.go b/namesys/namesys_test.go index a0ffbc50d80..b3e963c9e9e 100644 --- a/namesys/namesys_test.go +++ b/namesys/namesys_test.go @@ -11,7 +11,7 @@ import ( offroute "github.com/ipfs/go-ipfs-routing/offline" ipns "github.com/ipfs/go-ipns" path "github.com/ipfs/go-path" - "github.com/ipfs/go-unixfs" + unixfs "github.com/ipfs/go-unixfs" opts "github.com/ipfs/interface-go-ipfs-core/options/namesys" ci "github.com/libp2p/go-libp2p-core/crypto" peer "github.com/libp2p/go-libp2p-core/peer" @@ -49,10 +49,12 @@ func (r *mockResolver) resolveOnceAsync(ctx context.Context, name string, option func mockResolverOne() *mockResolver { return &mockResolver{ entries: map[string]string{ - "QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy": "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", - "QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n": "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", - "QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD": "/ipns/ipfs.io", - "QmQ4QZh8nrsczdUEwTyfBope4THUhqxqc1fx6qYhhzZQei": "/ipfs/QmP3ouCnU8NNLsW6261pAx2pNLV2E4dQoisB1sgda12Act", + "QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy": "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", + "QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n": "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", + "QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD": "/ipns/ipfs.io", + "QmQ4QZh8nrsczdUEwTyfBope4THUhqxqc1fx6qYhhzZQei": "/ipfs/QmP3ouCnU8NNLsW6261pAx2pNLV2E4dQoisB1sgda12Act", + "12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5": "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", // ed25519+identity multihash + "bafzbeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm": "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", // cidv1 in base32 with libp2p-key multicodec }, } } @@ -82,6 +84,8 @@ func TestNamesysResolution(t *testing.T) { testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 1, "/ipns/ipfs.io", ErrResolveRecursion) testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 2, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", ErrResolveRecursion) testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 3, "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", ErrResolveRecursion) + testResolution(t, r, "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", 1, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", ErrResolveRecursion) + testResolution(t, r, "/ipns/bafzbeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", 1, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", ErrResolveRecursion) } func TestPublishWithCache0(t *testing.T) { diff --git a/namesys/routing.go b/namesys/routing.go index c2d0d0252d4..60928fbca69 100644 --- a/namesys/routing.go +++ b/namesys/routing.go @@ -59,6 +59,7 @@ func (r *IpnsResolver) resolveOnceAsync(ctx context.Context, name string, option } name = strings.TrimPrefix(name, "/ipns/") + pid, err := peer.Decode(name) if err != nil { log.Debugf("RoutingResolver: could not convert public key hash %s to peer ID: %s\n", name, err) diff --git a/test/sharness/t0111-gateway-writeable.sh b/test/sharness/t0111-gateway-writeable.sh index f63b49bcf4b..6708b91a6db 100755 --- a/test/sharness/t0111-gateway-writeable.sh +++ b/test/sharness/t0111-gateway-writeable.sh @@ -41,7 +41,7 @@ test_expect_success "HTTP gateway gives access to sample file" ' test_expect_success "HTTP POST file gives Hash" ' echo "$RANDOM" >infile && - URL="http://localhost:$port/ipfs/" && + URL="http://127.0.0.1:$port/ipfs/" && curl -svX POST --data-binary @infile "$URL" 2>curl_post.out && grep "HTTP/1.1 201 Created" curl_post.out && LOCATION=$(grep Location curl_post.out) && @@ -49,7 +49,7 @@ test_expect_success "HTTP POST file gives Hash" ' ' test_expect_success "We can HTTP GET file just created" ' - URL="http://localhost:${port}${HASH}" && + URL="http://127.0.0.1:${port}${HASH}" && curl -so outfile "$URL" && test_cmp infile outfile ' @@ -60,7 +60,7 @@ test_expect_success "We got the correct hash" ' ' test_expect_success "HTTP GET empty directory" ' - URL="http://localhost:$port/ipfs/$HASH_EMPTY_DIR/" && + URL="http://127.0.0.1:$port/ipfs/$HASH_EMPTY_DIR/" && echo "GET $URL" && curl -so outfile "$URL" 2>curl_getEmpty.out && grep "Index of /ipfs/$HASH_EMPTY_DIR/" outfile @@ -68,7 +68,7 @@ test_expect_success "HTTP GET empty directory" ' test_expect_success "HTTP PUT file to construct a hierarchy" ' echo "$RANDOM" >infile && - URL="http://localhost:$port/ipfs/$HASH_EMPTY_DIR/test.txt" && + URL="http://127.0.0.1:$port/ipfs/$HASH_EMPTY_DIR/test.txt" && echo "PUT $URL" && curl -svX PUT --data-binary @infile "$URL" 2>curl_put.out && grep "HTTP/1.1 201 Created" curl_put.out && @@ -77,7 +77,7 @@ test_expect_success "HTTP PUT file to construct a hierarchy" ' ' test_expect_success "We can HTTP GET file just created" ' - URL="http://localhost:$port/ipfs/$HASH/test.txt" && + URL="http://127.0.0.1:$port/ipfs/$HASH/test.txt" && echo "GET $URL" && curl -so outfile "$URL" && test_cmp infile outfile @@ -85,7 +85,7 @@ test_expect_success "We can HTTP GET file just created" ' test_expect_success "HTTP PUT file to append to existing hierarchy" ' echo "$RANDOM" >infile2 && - URL="http://localhost:$port/ipfs/$HASH/test/test.txt" && + URL="http://127.0.0.1:$port/ipfs/$HASH/test/test.txt" && echo "PUT $URL" && curl -svX PUT --data-binary @infile2 "$URL" 2>curl_putAgain.out && grep "HTTP/1.1 201 Created" curl_putAgain.out && @@ -95,7 +95,7 @@ test_expect_success "HTTP PUT file to append to existing hierarchy" ' test_expect_success "We can HTTP GET file just updated" ' - URL="http://localhost:$port/ipfs/$HASH/test/test.txt" && + URL="http://127.0.0.1:$port/ipfs/$HASH/test/test.txt" && echo "GET $URL" && curl -svo outfile2 "$URL" 2>curl_getAgain.out && test_cmp infile2 outfile2 @@ -103,7 +103,7 @@ test_expect_success "We can HTTP GET file just updated" ' test_expect_success "HTTP PUT to replace a directory" ' echo "$RANDOM" >infile3 && - URL="http://localhost:$port/ipfs/$HASH/test" && + URL="http://127.0.0.1:$port/ipfs/$HASH/test" && echo "PUT $URL" && curl -svX PUT --data-binary @infile3 "$URL" 2>curl_putOverDirectory.out && grep "HTTP/1.1 201 Created" curl_putOverDirectory.out && @@ -112,7 +112,7 @@ test_expect_success "HTTP PUT to replace a directory" ' ' test_expect_success "We can HTTP GET file just put over a directory" ' - URL="http://localhost:$port/ipfs/$HASH/test" && + URL="http://127.0.0.1:$port/ipfs/$HASH/test" && echo "GET $URL" && curl -svo outfile3 "$URL" 2>curl_getOverDirectory.out && test_cmp infile3 outfile3 @@ -120,7 +120,7 @@ test_expect_success "We can HTTP GET file just put over a directory" ' test_expect_success "HTTP PUT to /ipns fails" ' PEERID=`ipfs id --format=""` && - URL="http://localhost:$port/ipns/$PEERID/test.txt" && + URL="http://127.0.0.1:$port/ipns/$PEERID/test.txt" && echo "PUT $URL" && curl -svX PUT --data-binary @infile1 "$URL" 2>curl_putIpns.out && grep "HTTP/1.1 400 Bad Request" curl_putIpns.out diff --git a/test/sharness/t0114-gateway-subdomains.sh b/test/sharness/t0114-gateway-subdomains.sh new file mode 100755 index 00000000000..d079f56e6c7 --- /dev/null +++ b/test/sharness/t0114-gateway-subdomains.sh @@ -0,0 +1,662 @@ +#!/usr/bin/env bash +# +# Copyright (c) Protocol Labs + +test_description="Test subdomain support on the HTTP gateway" + + +. lib/test-lib.sh + +## ============================================================================ +## Helpers specific to subdomain tests +## ============================================================================ + +# Helper that tests gateway response over direct HTTP +# and in all supported HTTP proxy modes +test_localhost_gateway_response_should_contain() { + local label="$1" + local expected="$3" + + # explicit "Host: $hostname" header to match browser behavior + # and also make tests independent from DNS + local host=$(echo $2 | cut -d'/' -f3 | cut -d':' -f1) + local hostname=$(echo $2 | cut -d'/' -f3 | cut -d':' -f1,2) + + # Proxy is the same as HTTP Gateway, we use raw IP and port to be sure + local proxy="http://127.0.0.1:$GWAY_PORT" + + # Create a raw URL version with IP to ensure hostname from Host header is used + # (removes false-positives, Host header is used for passing hostname already) + local url="$2" + local rawurl=$(echo "$url" | sed "s/$hostname/127.0.0.1:$GWAY_PORT/") + + #echo "hostname: $hostname" + #echo "url before: $url" + #echo "url after: $rawurl" + + # regular HTTP request + # (hostname in Host header, raw IP in URL) + test_expect_success "$label (direct HTTP)" " + curl -H \"Host: $hostname\" -sD - \"$rawurl\" > response && + test_should_contain \"$expected\" response + " + + # HTTP proxy + # (hostname is passed via URL) + # Note: proxy client should not care, but curl does DNS lookup + # for some reason anyway, so we pass static DNS mapping + test_expect_success "$label (HTTP proxy)" " + curl -x $proxy --resolve $hostname:127.0.0.1 -sD - \"$url\" > response && + test_should_contain \"$expected\" response + " + + # HTTP proxy 1.0 + # (repeating proxy test with older spec, just to be sure) + test_expect_success "$label (HTTP proxy 1.0)" " + curl --proxy1.0 $proxy --resolve $hostname:127.0.0.1 -sD - \"$url\" > response && + test_should_contain \"$expected\" response + " + + # HTTP proxy tunneling (CONNECT) + # https://tools.ietf.org/html/rfc7231#section-4.3.6 + # In HTTP/1.x, the pseudo-method CONNECT + # can be used to convert an HTTP connection into a tunnel to a remote host + test_expect_success "$label (HTTP proxy tunneling)" " + curl --proxytunnel -x $proxy -H \"Host: $hostname\" -sD - \"$rawurl\" > response && + test_should_contain \"$expected\" response + " +} + +# Helper that checks gateway resonse for specific hostname in Host header +test_hostname_gateway_response_should_contain() { + local label="$1" + local hostname="$2" + local url="$3" + local rawurl=$(echo "$url" | sed "s/$hostname/127.0.0.1:$GWAY_PORT/") + local expected="$4" + test_expect_success "$label" " + curl -H \"Host: $hostname\" -sD - \"$rawurl\" > response && + test_should_contain \"$expected\" response + " +} + +## ============================================================================ +## Start IPFS Node and prepare test CIDs +## ============================================================================ + +test_init_ipfs +test_launch_ipfs_daemon --offline + +# CIDv0to1 is necessary because raw-leaves are enabled by default during +# "ipfs add" with CIDv1 and disabled with CIDv0 +test_expect_success "Add test text file" ' + CID_VAL="hello" + CIDv1=$(echo $CID_VAL | ipfs add --cid-version 1 -Q) + CIDv0=$(echo $CID_VAL | ipfs add --cid-version 0 -Q) + CIDv0to1=$(echo "$CIDv0" | ipfs cid base32) +' + +test_expect_success "Add the test directory" ' + mkdir -p testdirlisting/subdir1/subdir2 && + echo "hello" > testdirlisting/hello && + echo "subdir2-bar" > testdirlisting/subdir1/subdir2/bar && + mkdir -p testdirlisting/api && + mkdir -p testdirlisting/ipfs && + echo "I am a txt file" > testdirlisting/api/file.txt && + echo "I am a txt file" > testdirlisting/ipfs/file.txt && + DIR_CID=$(ipfs add -Qr --cid-version 1 testdirlisting) +' + +test_expect_success "Publish test text file to IPNS" ' + PEERID=$(ipfs id --format="") + IPNS_IDv0=$(echo "$PEERID" | ipfs cid format -v 0) + IPNS_IDv1=$(echo "$PEERID" | ipfs cid format -v 1 --codec libp2p-key -b base32) + IPNS_IDv1_DAGPB=$(echo "$IPNS_IDv0" | ipfs cid format -v 1 -b base32) + test_check_peerid "${PEERID}" && + ipfs name publish --allow-offline -Q "/ipfs/$CIDv1" > name_publish_out && + ipfs name resolve "$PEERID" > output && + printf "/ipfs/%s\n" "$CIDv1" > expected2 && + test_cmp expected2 output +' + + +# ensure we start with empty Gateway.PublicGateways +test_expect_success 'start daemon with empty config for Gateway.PublicGateways' ' + test_kill_ipfs_daemon && + ipfs config --json Gateway.PublicGateways "{}" && + test_launch_ipfs_daemon --offline +' + +## ============================================================================ +## Test path-based requests to a local gateway with default config +## (forced redirects to http://*.localhost) +## ============================================================================ + +# /ipfs/ + +# IP remains old school path-based gateway + +test_localhost_gateway_response_should_contain \ + "request for 127.0.0.1/ipfs/{CID} stays on path" \ + "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \ + "$CID_VAL" + +# 'localhost' hostname is used for subdomains, and should not return +# payload directly, but redirect to URL with proper origin isolation + +test_localhost_gateway_response_should_contain \ + "request for localhost/ipfs/{CIDv1} returns status code HTTP 301" \ + "http://localhost:$GWAY_PORT/ipfs/$CIDv1" \ + "301 Moved Permanently" + +test_localhost_gateway_response_should_contain \ + "request for localhost/ipfs/{CIDv1} returns Location HTTP header for subdomain redirect in browsers" \ + "http://localhost:$GWAY_PORT/ipfs/$CIDv1" \ + "Location: http://$CIDv1.ipfs.localhost:$GWAY_PORT/" + +# Responses to the root domain of subdomain gateway hostname should Clear-Site-Data +# https://github.com/ipfs/go-ipfs/issues/6975#issuecomment-597472477 +test_localhost_gateway_response_should_contain \ + "request for localhost/ipfs/{CIDv1} returns Clear-Site-Data header to purge Origin cookies and storage" \ + "http://localhost:$GWAY_PORT/ipfs/$CIDv1" \ + 'Clear-Site-Data: \"cookies\", \"storage\"' + +# We return body with HTTP 301 so existing cli scripts that use path-based +# gateway do not break (curl doesn't auto-redirect without passing -L; wget +# does not span across hostnames by default) +# Context: https://github.com/ipfs/go-ipfs/issues/6975 +test_localhost_gateway_response_should_contain \ + "request for localhost/ipfs/{CIDv1} includes valid payload in body for CLI tools like curl" \ + "http://localhost:$GWAY_PORT/ipfs/$CIDv1" \ + "$CID_VAL" + +test_localhost_gateway_response_should_contain \ + "request for localhost/ipfs/{CIDv0} redirects to CIDv1 representation in subdomain" \ + "http://localhost:$GWAY_PORT/ipfs/$CIDv0" \ + "Location: http://${CIDv0to1}.ipfs.localhost:$GWAY_PORT/" + +# /ipns/ + +test_localhost_gateway_response_should_contain \ + "request for localhost/ipns/{CIDv0} redirects to CIDv1 with libp2p-key multicodec in subdomain" \ + "http://localhost:$GWAY_PORT/ipns/$IPNS_IDv0" \ + "Location: http://${IPNS_IDv1}.ipns.localhost:$GWAY_PORT/" + +# /ipns/ + +test_localhost_gateway_response_should_contain \ + "request for localhost/ipns/{fqdn} redirects to DNSLink in subdomain" \ + "http://localhost:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki" \ + "Location: http://en.wikipedia-on-ipfs.org.ipns.localhost:$GWAY_PORT/wiki" + +# API on localhost subdomain gateway + +# /api/v0 present on the root hostname +test_localhost_gateway_response_should_contain \ + "request for localhost/api" \ + "http://localhost:$GWAY_PORT/api/v0/refs?arg=${DIR_CID}&r=true" \ + "Ref" + +# /api/v0 not mounted on content root subdomains +test_localhost_gateway_response_should_contain \ + "request for {cid}.ipfs.localhost/api returns data if present on the content root" \ + "http://${DIR_CID}.ipfs.localhost:$GWAY_PORT/api/file.txt" \ + "I am a txt file" + +test_localhost_gateway_response_should_contain \ + "request for {cid}.ipfs.localhost/api/v0/refs returns 404" \ + "http://${DIR_CID}.ipfs.localhost:$GWAY_PORT/api/v0/refs?arg=${DIR_CID}&r=true" \ + "404 Not Found" + +## ============================================================================ +## Test subdomain-based requests to a local gateway with default config +## (origin per content root at http://*.localhost) +## ============================================================================ + +# {CID}.ipfs.localhost + +test_localhost_gateway_response_should_contain \ + "request for {CID}.ipfs.localhost should return expected payload" \ + "http://${CIDv1}.ipfs.localhost:$GWAY_PORT" \ + "$CID_VAL" + +# ensure /ipfs/ namespace is not mounted on subdomain +test_localhost_gateway_response_should_contain \ + "request for {CID}.ipfs.localhost/ipfs/{CID} should return HTTP 404" \ + "http://${CIDv1}.ipfs.localhost:$GWAY_PORT/ipfs/$CIDv1" \ + "404 Not Found" + +# ensure requests to /ipfs/* are not blocked, if content root has such subdirectory +test_localhost_gateway_response_should_contain \ + "request for {CID}.ipfs.localhost/ipfs/file.txt should return data from a file in CID content root" \ + "http://${DIR_CID}.ipfs.localhost:$GWAY_PORT/ipfs/file.txt" \ + "I am a txt file" + +# {CID}.ipfs.localhost/sub/dir (Directory Listing) +DIR_HOSTNAME="${DIR_CID}.ipfs.localhost:$GWAY_PORT" + +test_expect_success "valid file and subdirectory paths in directory listing at {cid}.ipfs.localhost" ' + curl -s --resolve $DIR_HOSTNAME:127.0.0.1 "http://$DIR_HOSTNAME" > list_response && + test_should_contain "hello" list_response && + test_should_contain "subdir1" list_response +' + +test_expect_success "valid parent directory path in directory listing at {cid}.ipfs.localhost/sub/dir" ' + curl -s --resolve $DIR_HOSTNAME:127.0.0.1 "http://$DIR_HOSTNAME/subdir1/subdir2/" > list_response && + test_should_contain ".." list_response && + test_should_contain "bar" list_response +' + +test_expect_success "request for deep path resource at {cid}.ipfs.localhost/sub/dir/file" ' + curl -s --resolve $DIR_HOSTNAME:127.0.0.1 "http://$DIR_HOSTNAME/subdir1/subdir2/bar" > list_response && + test_should_contain "subdir2-bar" list_response +' + +# *.ipns.localhost + +# .ipns.localhost + +test_localhost_gateway_response_should_contain \ + "request for {CIDv1-libp2p-key}.ipns.localhost returns expected payload" \ + "http://${IPNS_IDv1}.ipns.localhost:$GWAY_PORT" \ + "$CID_VAL" + +test_localhost_gateway_response_should_contain \ + "request for {CIDv1-dag-pb}.ipns.localhost redirects to CID with libp2p-key multicodec" \ + "http://${IPNS_IDv1_DAGPB}.ipns.localhost:$GWAY_PORT" \ + "Location: http://${IPNS_IDv1}.ipns.localhost:$GWAY_PORT/" + +# .ipns.localhost + +# DNSLink test requires a daemon in online mode with precached /ipns/ mapping +test_kill_ipfs_daemon +DNSLINK_FQDN="dnslink-test.example.com" +export IPFS_NS_MAP="$DNSLINK_FQDN:/ipfs/$CIDv1" +test_launch_ipfs_daemon + +test_localhost_gateway_response_should_contain \ + "request for {dnslink}.ipns.localhost returns expected payload" \ + "http://$DNSLINK_FQDN.ipns.localhost:$GWAY_PORT" \ + "$CID_VAL" + +# api.localhost/api + +# Note: we use DIR_CID so refs -r returns some CIDs for child nodes +test_localhost_gateway_response_should_contain \ + "request for api.localhost returns API response" \ + "http://api.localhost:$GWAY_PORT/api/v0/refs?arg=$DIR_CID&r=true" \ + "Ref" + +## ============================================================================ +## Test subdomain-based requests with a custom hostname config +## (origin per content root at http://*.example.com) +## ============================================================================ + +# set explicit subdomain gateway config for the hostname +ipfs config --json Gateway.PublicGateways '{ + "example.com": { + "UseSubdomains": true, + "Paths": ["/ipfs", "/ipns", "/api"] + } +}' || exit 1 +# restart daemon to apply config changes +test_kill_ipfs_daemon +test_launch_ipfs_daemon --offline + + +# example.com/ip(f|n)s/* +# ============================================================================= + +# path requests to the root hostname should redirect +# to a subdomain URL with proper origin isolation + +test_hostname_gateway_response_should_contain \ + "request for example.com/ipfs/{CIDv1} produces redirect to {CIDv1}.ipfs.example.com" \ + "example.com" \ + "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \ + "Location: http://$CIDv1.ipfs.example.com/" + +# error message should include original CID +# (and it should be case-sensitive, as we can't assume everyone uses base32) +test_hostname_gateway_response_should_contain \ + "request for example.com/ipfs/{InvalidCID} produces useful error before redirect" \ + "example.com" \ + "http://127.0.0.1:$GWAY_PORT/ipfs/QmInvalidCID" \ + 'invalid path \"/ipfs/QmInvalidCID\"' + +test_hostname_gateway_response_should_contain \ + "request for example.com/ipfs/{CIDv0} produces redirect to {CIDv1}.ipfs.example.com" \ + "example.com" \ + "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv0" \ + "Location: http://${CIDv0to1}.ipfs.example.com/" + +# Support X-Forwarded-Proto +test_expect_success "request for http://example.com/ipfs/{CID} with X-Forwarded-Proto: https produces redirect to HTTPS URL" " + curl -H \"X-Forwarded-Proto: https\" -H \"Host: example.com\" -sD - \"http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1\" > response && + test_should_contain \"Location: https://$CIDv1.ipfs.example.com/\" response +" + + + +# example.com/ipns/ + +test_hostname_gateway_response_should_contain \ + "request for example.com/ipns/{CIDv0} redirects to CIDv1 with libp2p-key multicodec in subdomain" \ + "example.com" \ + "http://127.0.0.1:$GWAY_PORT/ipns/$IPNS_IDv0" \ + "Location: http://${IPNS_IDv1}.ipns.example.com/" + +# example.com/ipns/ + +test_hostname_gateway_response_should_contain \ + "request for example.com/ipns/{fqdn} redirects to DNSLink in subdomain" \ + "example.com" \ + "http://127.0.0.1:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki" \ + "Location: http://en.wikipedia-on-ipfs.org.ipns.example.com/wiki" + +# *.ipfs.example.com: subdomain requests made with custom FQDN in Host header + +test_hostname_gateway_response_should_contain \ + "request for {CID}.ipfs.example.com should return expected payload" \ + "${CIDv1}.ipfs.example.com" \ + "http://127.0.0.1:$GWAY_PORT/" \ + "$CID_VAL" + +test_hostname_gateway_response_should_contain \ + "request for {CID}.ipfs.example.com/ipfs/{CID} should return HTTP 404" \ + "${CIDv1}.ipfs.example.com" \ + "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \ + "404 Not Found" + +# {CID}.ipfs.example.com/sub/dir (Directory Listing) +DIR_FQDN="${DIR_CID}.ipfs.example.com" + +test_expect_success "valid file and directory paths in directory listing at {cid}.ipfs.example.com" ' + curl -s -H "Host: $DIR_FQDN" http://127.0.0.1:$GWAY_PORT > list_response && + test_should_contain "hello" list_response && + test_should_contain "subdir1" list_response +' + +test_expect_success "valid parent directory path in directory listing at {cid}.ipfs.example.com/sub/dir" ' + curl -s -H "Host: $DIR_FQDN" http://127.0.0.1:$GWAY_PORT/subdir1/subdir2/ > list_response && + test_should_contain ".." list_response && + test_should_contain "bar" list_response +' + +test_expect_success "request for deep path resource {cid}.ipfs.example.com/sub/dir/file" ' + curl -s -H "Host: $DIR_FQDN" http://127.0.0.1:$GWAY_PORT/subdir1/subdir2/bar > list_response && + test_should_contain "subdir2-bar" list_response +' + +# *.ipns.example.com +# ============================================================================ + +# .ipns.example.com + +test_hostname_gateway_response_should_contain \ + "request for {CIDv1-libp2p-key}.ipns.example.com returns expected payload" \ + "${IPNS_IDv1}.ipns.example.com" \ + "http://127.0.0.1:$GWAY_PORT" \ + "$CID_VAL" + +test_hostname_gateway_response_should_contain \ + "request for {CIDv1-dag-pb}.ipns.localhost redirects to CID with libp2p-key multicodec" \ + "${IPNS_IDv1_DAGPB}.ipns.example.com" \ + "http://127.0.0.1:$GWAY_PORT" \ + "Location: http://${IPNS_IDv1}.ipns.example.com/" + +# API on subdomain gateway example.com +# ============================================================================ + +# present at the root domain +test_hostname_gateway_response_should_contain \ + "request for example.com/api/v0/refs returns expected payload when /api is on Paths whitelist" \ + "example.com" \ + "http://127.0.0.1:$GWAY_PORT/api/v0/refs?arg=${DIR_CID}&r=true" \ + "Ref" + +# not mounted on content root subdomains +test_hostname_gateway_response_should_contain \ + "request for {cid}.ipfs.example.com/api returns data if present on the content root" \ + "$DIR_CID.ipfs.example.com" \ + "http://127.0.0.1:$GWAY_PORT/api/file.txt" \ + "I am a txt file" + +test_hostname_gateway_response_should_contain \ + "request for {cid}.ipfs.example.com/api/v0/refs returns 404" \ + "$CIDv1.ipfs.example.com" \ + "http://127.0.0.1:$GWAY_PORT/api/v0/refs?arg=${DIR_CID}&r=true" \ + "404 Not Found" + +# disable /api on example.com +ipfs config --json Gateway.PublicGateways '{ + "example.com": { + "UseSubdomains": true, + "Paths": ["/ipfs", "/ipns"] + } +}' || exit 1 +# restart daemon to apply config changes +test_kill_ipfs_daemon +test_launch_ipfs_daemon --offline + +# not mounted at the root domain +test_hostname_gateway_response_should_contain \ + "request for example.com/api/v0/refs returns 404 if /api not on Paths whitelist" \ + "example.com" \ + "http://127.0.0.1:$GWAY_PORT/api/v0/refs?arg=${DIR_CID}&r=true" \ + "404 Not Found" + +# not mounted on content root subdomains +test_hostname_gateway_response_should_contain \ + "request for {cid}.ipfs.example.com/api returns data if present on the content root" \ + "$DIR_CID.ipfs.example.com" \ + "http://127.0.0.1:$GWAY_PORT/api/file.txt" \ + "I am a txt file" + +# DNSLink: .ipns.example.com +# (not really useful outside of localhost, as setting TLS for more than one +# level of wildcard is a pain, but we support it if someone really wants it) +# ============================================================================ + +# DNSLink test requires a daemon in online mode with precached /ipns/ mapping +test_kill_ipfs_daemon +DNSLINK_FQDN="dnslink-subdomain-gw-test.example.org" +export IPFS_NS_MAP="$DNSLINK_FQDN:/ipfs/$CIDv1" +test_launch_ipfs_daemon + +test_hostname_gateway_response_should_contain \ + "request for {dnslink}.ipns.example.com returns expected payload" \ + "$DNSLINK_FQDN.ipns.example.com" \ + "http://127.0.0.1:$GWAY_PORT" \ + "$CID_VAL" + +# Disable selected Paths for the subdomain gateway hostname +# ============================================================================= + +# disable /ipns for the hostname by not whitelisting it +ipfs config --json Gateway.PublicGateways '{ + "example.com": { + "UseSubdomains": true, + "Paths": ["/ipfs"] + } +}' || exit 1 +# restart daemon to apply config changes +test_kill_ipfs_daemon +test_launch_ipfs_daemon --offline + +# refuse requests to Paths that were not explicitly whitelisted for the hostname +test_hostname_gateway_response_should_contain \ + "request for *.ipns.example.com returns HTTP 404 Not Found when /ipns is not on Paths whitelist" \ + "${IPNS_IDv1}.ipns.example.com" \ + "http://127.0.0.1:$GWAY_PORT" \ + "404 Not Found" + + +## ============================================================================ +## Test path-based requests with a custom hostname config +## ============================================================================ + +# set explicit subdomain gateway config for the hostname +ipfs config --json Gateway.PublicGateways '{ + "example.com": { + "UseSubdomains": false, + "Paths": ["/ipfs"] + } +}' || exit 1 + +# restart daemon to apply config changes +test_kill_ipfs_daemon +test_launch_ipfs_daemon --offline + +# example.com/ip(f|n)s/* smoke-tests +# ============================================================================= + +# confirm path gateway works for /ipfs +test_hostname_gateway_response_should_contain \ + "request for example.com/ipfs/{CIDv1} returns expected payload" \ + "example.com" \ + "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \ + "$CID_VAL" + +# refuse subdomain requests on path gateway +# (we don't want false sense of security) +test_hostname_gateway_response_should_contain \ + "request for {CID}.ipfs.example.com/ipfs/{CID} should return HTTP 404 Not Found" \ + "${CIDv1}.ipfs.example.com" \ + "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \ + "404 Not Found" + +# refuse requests to Paths that were not explicitly whitelisted for the hostname +test_hostname_gateway_response_should_contain \ + "request for example.com/ipns/ returns HTTP 404 Not Found when /ipns is not on Paths whitelist" \ + "example.com" \ + "http://127.0.0.1:$GWAY_PORT/ipns/$IPNS_IDv1" \ + "404 Not Found" + +## ============================================================================ +## Test DNSLink requests with a custom PublicGateway (hostname config) +## (DNSLink site at http://dnslink-test.example.com) +## ============================================================================ + +test_kill_ipfs_daemon + +# disable wildcard DNSLink gateway +# and enable it on specific NSLink hostname +ipfs config --json Gateway.NoDNSLink true && \ +ipfs config --json Gateway.PublicGateways '{ + "dnslink-enabled-on-fqdn.example.org": { + "NoDNSLink": false, + "UseSubdomains": false, + "Paths": ["/ipfs"] + }, + "only-dnslink-enabled-on-fqdn.example.org": { + "NoDNSLink": false, + "UseSubdomains": false, + "Paths": [] + }, + "dnslink-disabled-on-fqdn.example.com": { + "NoDNSLink": true, + "UseSubdomains": false, + "Paths": [] + } +}' || exit 1 + +# DNSLink test requires a daemon in online mode with precached /ipns/ mapping +DNSLINK_FQDN="dnslink-enabled-on-fqdn.example.org" +ONLY_DNSLINK_FQDN="only-dnslink-enabled-on-fqdn.example.org" +NO_DNSLINK_FQDN="dnslink-disabled-on-fqdn.example.com" +export IPFS_NS_MAP="$DNSLINK_FQDN:/ipfs/$CIDv1,$ONLY_DNSLINK_FQDN:/ipfs/$DIR_CID" + +# restart daemon to apply config changes +test_launch_ipfs_daemon + +# make sure test setup is valid (fail if CoreAPI is unable to resolve) +test_expect_success "spoofed DNSLink record resolves in cli" " + ipfs resolve /ipns/$DNSLINK_FQDN > result && + test_should_contain \"$CIDv1\" result && + ipfs cat /ipns/$DNSLINK_FQDN > result && + test_should_contain \"$CID_VAL\" result +" + +# DNSLink enabled + +test_hostname_gateway_response_should_contain \ + "request for http://{dnslink-fqdn}/ PublicGateway returns expected payload" \ + "$DNSLINK_FQDN" \ + "http://127.0.0.1:$GWAY_PORT/" \ + "$CID_VAL" + +test_hostname_gateway_response_should_contain \ + "request for {dnslink-fqdn}/ipfs/{cid} returns expected payload when /ipfs is on Paths whitelist" \ + "$DNSLINK_FQDN" \ + "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv1" \ + "$CID_VAL" + +# Test for a fun edge case: DNSLink-only gateway without /ipfs/ namespace +# mounted, and with subdirectory named "ipfs" ¯\_(ツ)_/¯ +test_hostname_gateway_response_should_contain \ + "request for {dnslink-fqdn}/ipfs/file.txt returns data from content root when /ipfs in not on Paths whitelist" \ + "$ONLY_DNSLINK_FQDN" \ + "http://127.0.0.1:$GWAY_PORT/ipfs/file.txt" \ + "I am a txt file" + +test_hostname_gateway_response_should_contain \ + "request for {dnslink-fqdn}/ipns/{peerid} returns 404 when path is not whitelisted" \ + "$DNSLINK_FQDN" \ + "http://127.0.0.1:$GWAY_PORT/ipns/$IPNS_IDv0" \ + "404 Not Found" + +# DNSLink disabled + +test_hostname_gateway_response_should_contain \ + "request for http://{dnslink-fqdn}/ returns 404 when NoDNSLink=true" \ + "$NO_DNSLINK_FQDN" \ + "http://127.0.0.1:$GWAY_PORT/" \ + "404 Not Found" + +test_hostname_gateway_response_should_contain \ + "request for {dnslink-fqdn}/ipfs/{cid} returns 404 when path is not whitelisted" \ + "$NO_DNSLINK_FQDN" \ + "http://127.0.0.1:$GWAY_PORT/ipfs/$CIDv0" \ + "404 Not Found" + + +## ============================================================================ +## Test wildcard DNSLink (any hostname, with default config) +## ============================================================================ + +test_kill_ipfs_daemon + +# enable wildcard DNSLink gateway (any value in Host header) +# and remove custom PublicGateways +ipfs config --json Gateway.NoDNSLink false && \ +ipfs config --json Gateway.PublicGateways '{}' || exit 1 + +# DNSLink test requires a daemon in online mode with precached /ipns/ mapping +DNSLINK_FQDN="wildcard-dnslink-not-in-config.example.com" +export IPFS_NS_MAP="$DNSLINK_FQDN:/ipfs/$CIDv1" + +# restart daemon to apply config changes +test_launch_ipfs_daemon + +# make sure test setup is valid (fail if CoreAPI is unable to resolve) +test_expect_success "spoofed DNSLink record resolves in cli" " + ipfs resolve /ipns/$DNSLINK_FQDN > result && + test_should_contain \"$CIDv1\" result && + ipfs cat /ipns/$DNSLINK_FQDN > result && + test_should_contain \"$CID_VAL\" result +" + +# gateway test +test_hostname_gateway_response_should_contain \ + "request for http://{dnslink-fqdn}/ (wildcard) returns expected payload" \ + "$DNSLINK_FQDN" \ + "http://127.0.0.1:$GWAY_PORT/" \ + "$CID_VAL" + +# ============================================================================= +# ensure we end with empty Gateway.PublicGateways +ipfs config --json Gateway.PublicGateways '{}' +test_kill_ipfs_daemon + +test_done diff --git a/test/sharness/t0120-bootstrap.sh b/test/sharness/t0120-bootstrap.sh index 883e29a8be6..538c5bde0ef 100755 --- a/test/sharness/t0120-bootstrap.sh +++ b/test/sharness/t0120-bootstrap.sh @@ -10,14 +10,6 @@ BP2="/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19 BP3="/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb" BP4="/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt" BP5="/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ" -BP6="/ip4/104.236.179.241/tcp/4001/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM" -BP7="/ip4/104.236.76.40/tcp/4001/p2p/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64" -BP8="/ip4/128.199.219.111/tcp/4001/p2p/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu" -BP9="/ip4/178.62.158.247/tcp/4001/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd" -BP10="/ip6/2400:6180:0:d0::151:6001/tcp/4001/p2p/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu" -BP11="/ip6/2604:a880:1:20::203:d001/tcp/4001/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM" -BP12="/ip6/2604:a880:800:10::4a:5001/tcp/4001/p2p/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64" -BP13="/ip6/2a03:b0c0:0:1010::23:1001/tcp/4001/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd" test_description="Test ipfs repo operations" @@ -99,18 +91,10 @@ test_bootstrap_cmd() { echo "added $BP3" >>add2_expected && echo "added $BP4" >>add2_expected && echo "added $BP5" >>add2_expected && - echo "added $BP6" >>add2_expected && - echo "added $BP7" >>add2_expected && - echo "added $BP8" >>add2_expected && - echo "added $BP9" >>add2_expected && - echo "added $BP10" >>add2_expected && - echo "added $BP11" >>add2_expected && - echo "added $BP12" >>add2_expected && - echo "added $BP13" >>add2_expected && test_cmp add2_expected add2_actual ' - test_bootstrap_list_cmd $BP1 $BP2 $BP3 $BP4 $BP5 $BP6 $BP7 $BP8 $BP9 $BP10 $BP11 $BP12 $BP13 $BP14 $BP15 $BP16 $BP17 + test_bootstrap_list_cmd $BP1 $BP2 $BP3 $BP4 $BP5 test_expect_success "'ipfs bootstrap rm --all' succeeds" ' ipfs bootstrap rm --all >rm2_actual @@ -122,14 +106,6 @@ test_bootstrap_cmd() { echo "removed $BP3" >>rm2_expected && echo "removed $BP4" >>rm2_expected && echo "removed $BP5" >>rm2_expected && - echo "removed $BP6" >>rm2_expected && - echo "removed $BP7" >>rm2_expected && - echo "removed $BP8" >>rm2_expected && - echo "removed $BP9" >>rm2_expected && - echo "removed $BP10" >>rm2_expected && - echo "removed $BP11" >>rm2_expected && - echo "removed $BP12" >>rm2_expected && - echo "removed $BP13" >>rm2_expected && test_cmp rm2_expected rm2_actual ' diff --git a/test/sharness/t0160-resolve.sh b/test/sharness/t0160-resolve.sh index 87740685644..62a6f5b3bfa 100755 --- a/test/sharness/t0160-resolve.sh +++ b/test/sharness/t0160-resolve.sh @@ -116,6 +116,15 @@ test_resolve_cmd_b32() { test_resolve_setup_name "self" "/ipfs/$c_hash_b32" test_resolve "/ipns/$self_hash" "/ipfs/$c_hash_b32" --cid-base=base32 + + # peer ID represented as CIDv1 require libp2p-key multicodec + # https://github.com/libp2p/specs/blob/master/RFC/0001-text-peerid-cid.md + local self_hash_b32protobuf=$(echo $self_hash | ipfs cid format -v 1 -b b --codec protobuf) + local self_hash_b32libp2pkey=$(echo $self_hash | ipfs cid format -v 1 -b b --codec libp2p-key) + test_expect_success "resolve of /ipns/{cidv1} with multicodec other than libp2p-key returns a meaningful error" ' + test_expect_code 1 ipfs resolve /ipns/$self_hash_b32protobuf 2>cidcodec_error && + grep "Error: peer ID represented as CIDv1 require libp2p-key multicodec: retry with /ipns/$self_hash_b32libp2pkey" cidcodec_error + ' } diff --git a/test/sharness/t0184-http-proxy-over-p2p.sh b/test/sharness/t0184-http-proxy-over-p2p.sh index 578a3f424df..06a3b9ccb18 100755 --- a/test/sharness/t0184-http-proxy-over-p2p.sh +++ b/test/sharness/t0184-http-proxy-over-p2p.sh @@ -144,10 +144,19 @@ test_expect_success 'configure nodes' ' iptb testbed create -type localipfs -count 2 -force -init && ipfsi 0 config --json Experimental.Libp2pStreamMounting true && ipfsi 1 config --json Experimental.Libp2pStreamMounting true && - ipfsi 0 config --json Experimental.P2pHttpProxy true + ipfsi 0 config --json Experimental.P2pHttpProxy true && ipfsi 0 config --json Addresses.Gateway "[\"/ip4/127.0.0.1/tcp/$IPFS_GATEWAY_PORT\"]" ' +test_expect_success 'configure a subdomain gateway with /p2p/ path whitelisted' " + ipfsi 0 config --json Gateway.PublicGateways '{ + \"example.com\": { + \"UseSubdomains\": true, + \"Paths\": [\"/p2p/\"] + } + }' +" + test_expect_success 'start and connect nodes' ' iptb start -wait && iptb connect 0 1 ' @@ -206,6 +215,30 @@ test_expect_success 'handle multipart/form-data http request' ' curl_send_multipart_form_request 200 ' +# subdomain gateway at *.p2p.example.com requires PeerdID in base32 +RECEIVER_ID_CIDv1=$( ipfs cid format -v 1 -b b --codec libp2p-key -- $RECEIVER_ID) + +# OK: $peerid.p2p.example.com/http/index.txt +test_expect_success "handle http request to a subdomain gateway" ' + serve_content "SUBDOMAIN PROVIDES ORIGIN ISOLATION PER RECEIVER_ID" && + curl -H "Host: $RECEIVER_ID_CIDv1.p2p.example.com" -sD - $SENDER_GATEWAY/http/index.txt > p2p_response && + test_should_contain "SUBDOMAIN PROVIDES ORIGIN ISOLATION PER RECEIVER_ID" p2p_response +' + +# FAIL: $peerid.p2p.example.com/p2p/$peerid/http/index.txt +test_expect_success "handle invalid http request to a subdomain gateway" ' + serve_content "SUBDOMAIN DOES NOT SUPPORT FULL /p2p/ PATH" && + curl -H "Host: $RECEIVER_ID_CIDv1.p2p.example.com" -sD - $SENDER_GATEWAY/p2p/$RECEIVER_ID/http/index.txt > p2p_response && + test_should_contain "400 Bad Request" p2p_response +' + +# REDIRECT: example.com/p2p/$peerid/http/index.txt → $peerid.p2p.example.com/http/index.txt +test_expect_success "redirect http path request to subdomain gateway" ' + serve_content "SUBDOMAIN ROOT REDIRECTS /p2p/ PATH TO SUBDOMAIN" && + curl -H "Host: example.com" -sD - $SENDER_GATEWAY/p2p/$RECEIVER_ID/http/index.txt > p2p_response && + test_should_contain "Location: http://$RECEIVER_ID_CIDv1.p2p.example.com/http/index.txt" p2p_response +' + test_expect_success 'stop http server' ' teardown_remote_server '