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