diff --git a/gateway/core/corehttp/corehttp.go b/gateway/core/corehttp/corehttp.go index c52bea8f5..d99a07691 100644 --- a/gateway/core/corehttp/corehttp.go +++ b/gateway/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/gateway/core/corehttp/gateway_handler.go b/gateway/core/corehttp/gateway_handler.go index de8038f53..d3c4d2639 100644 --- a/gateway/core/corehttp/gateway_handler.go +++ b/gateway/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/gateway/core/corehttp/gateway_test.go b/gateway/core/corehttp/gateway_test.go index 9128aa017..daf1af07c 100644 --- a/gateway/core/corehttp/gateway_test.go +++ b/gateway/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/gateway/core/corehttp/hostname.go b/gateway/core/corehttp/hostname.go new file mode 100644 index 000000000..143435106 --- /dev/null +++ b/gateway/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/gateway/core/corehttp/hostname_test.go b/gateway/core/corehttp/hostname_test.go new file mode 100644 index 000000000..9a2974648 --- /dev/null +++ b/gateway/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/gateway/core/corehttp/ipns_hostname.go b/gateway/core/corehttp/ipns_hostname.go deleted file mode 100644 index d5512779b..000000000 --- a/gateway/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/gateway/core/corehttp/proxy.go b/gateway/core/corehttp/p2p_proxy.go similarity index 94% rename from gateway/core/corehttp/proxy.go rename to gateway/core/corehttp/p2p_proxy.go index 17cb00528..0a615c33a 100644 --- a/gateway/core/corehttp/proxy.go +++ b/gateway/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/gateway/core/corehttp/proxy_test.go b/gateway/core/corehttp/p2p_proxy_test.go similarity index 100% rename from gateway/core/corehttp/proxy_test.go rename to gateway/core/corehttp/p2p_proxy_test.go