diff --git a/core/corehttp/gateway_handler.go b/core/corehttp/gateway_handler.go index 550943675..154db3b53 100644 --- a/core/corehttp/gateway_handler.go +++ b/core/corehttp/gateway_handler.go @@ -90,6 +90,19 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request urlPath := r.URL.Path + // IPNSHostnameOption might have constructed an IPNS 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 := urlPath + ipnsHostname := false + hdr := r.Header["X-IPNS-Original-Path"] + if len(hdr) > 0 { + originalUrlPath = hdr[0] + ipnsHostname = true + } + if i.config.BlockList != nil && i.config.BlockList.ShouldBlock(urlPath) { w.WriteHeader(http.StatusForbidden) w.Write([]byte("403 - Forbidden")) @@ -112,10 +125,17 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request w.Header().Set("X-IPFS-Path", urlPath) // Suborigin header, sandboxes apps from each other in the browser (even - // though they are served from the same gateway domain). NOTE: This is not - // yet widely supported by browsers. - pathRoot := strings.SplitN(urlPath, "/", 4)[2] - w.Header().Set("Suborigin", pathRoot) + // though they are served from the same gateway domain). + // + // Omited 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: example.net header. + // + // NOTE: This is not yet widely supported by browsers. + if !ipnsHostname { + pathRoot := strings.SplitN(urlPath, "/", 4)[2] + w.Header().Set("Suborigin", pathRoot) + } dr, err := uio.NewDagReader(ctx, nd, i.node.DAG) if err != nil && err != uio.ErrIsDir { @@ -150,13 +170,16 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request foundIndex := false for _, link := range nd.Links { if link.Name == "index.html" { + log.Debugf("found index.html link for %s", urlPath) + foundIndex = true + if urlPath[len(urlPath)-1] != '/' { - http.Redirect(w, r, urlPath+"/", 302) + // See comment above where originalUrlPath is declared. + http.Redirect(w, r, originalUrlPath+"/", 302) + log.Debugf("redirect to %s", originalUrlPath+"/") return } - log.Debug("found index") - foundIndex = true // return index page instead. nd, err := core.Resolve(ctx, i.node, path.Path(urlPath+"/index.html")) if err != nil { @@ -177,7 +200,8 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request break } - di := directoryItem{link.Size, link.Name, gopath.Join(urlPath, link.Name)} + // See comment above where originalUrlPath is declared. + di := directoryItem{link.Size, link.Name, gopath.Join(originalUrlPath, link.Name)} dirListing = append(dirListing, di) } @@ -185,7 +209,7 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request if r.Method != "HEAD" { // construct the correct back link // https://github.com/ipfs/go-ipfs/issues/1365 - var backLink string = r.URL.Path + var backLink string = urlPath // don't go further up than /ipfs/$hash/ pathSplit := strings.Split(backLink, "/") @@ -205,9 +229,20 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request } } + // strip /ipfs/$hash from backlink if IPNSHostnameOption touched the path. + if ipnsHostname { + backLink = "/" + if len(pathSplit) > 5 { + // also strip the trailing segment, because it's a backlink + backLinkParts := pathSplit[3 : len(pathSplit)-2] + backLink += strings.Join(backLinkParts, "/") + "/" + } + } + + // See comment above where originalUrlPath is declared. tplData := listingTemplateData{ Listing: dirListing, - Path: urlPath, + Path: originalUrlPath, BackLink: backLink, } err := listingTemplate.Execute(w, tplData) diff --git a/core/corehttp/gateway_test.go b/core/corehttp/gateway_test.go index 7ad3584da..a90bd641e 100644 --- a/core/corehttp/gateway_test.go +++ b/core/corehttp/gateway_test.go @@ -37,7 +37,7 @@ func (m mockNamesys) Publish(ctx context.Context, name ci.PrivKey, value path.Pa return errors.New("not implemented for mockNamesys") } -func newNodeWithMockNamesys(t *testing.T, ns mockNamesys) *core.IpfsNode { +func newNodeWithMockNamesys(ns mockNamesys) (*core.IpfsNode, error) { c := config.Config{ Identity: config.Identity{ PeerID: "Qmfoo", // required by offline node @@ -49,10 +49,10 @@ func newNodeWithMockNamesys(t *testing.T, ns mockNamesys) *core.IpfsNode { } n, err := core.NewIPFSNode(context.Background(), core.Offline(r)) if err != nil { - t.Fatal(err) + return nil, err } n.Namesys = ns - return n + return n, nil } type delegatedHandler struct { @@ -63,21 +63,30 @@ func (dh *delegatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { dh.Handler.ServeHTTP(w, r) } -func TestGatewayGet(t *testing.T) { - t.Skip("not sure whats going on here") - ns := mockNamesys{} - n := newNodeWithMockNamesys(t, ns) - k, err := coreunix.Add(n, strings.NewReader("fnord")) +func doWithoutRedirect(req *http.Request) (*http.Response, error) { + tag := "without-redirect" + c := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return errors.New(tag) + }, + } + res, err := c.Do(req) + if err != nil && !strings.Contains(err.Error(), tag) { + return nil, err + } + return res, nil +} + +func newTestServerAndNode(t *testing.T, ns mockNamesys) (*httptest.Server, *core.IpfsNode) { + n, err := newNodeWithMockNamesys(ns) if err != nil { t.Fatal(err) } - ns["example.com"] = path.FromString("/ipfs/" + k) // need this variable here since we need to construct handler with // listener, and server with handler. yay cycles. dh := &delegatedHandler{} ts := httptest.NewServer(dh) - defer ts.Close() dh.Handler, err = makeHandler(n, ts.Listener, @@ -88,6 +97,20 @@ func TestGatewayGet(t *testing.T) { t.Fatal(err) } + return ts, n +} + +func TestGatewayGet(t *testing.T) { + ns := mockNamesys{} + ts, n := newTestServerAndNode(t, ns) + defer ts.Close() + + k, err := coreunix.Add(n, strings.NewReader("fnord")) + if err != nil { + t.Fatal(err) + } + ns["/ipns/example.com"] = path.FromString("/ipfs/" + k) + t.Log(ts.URL) for _, test := range []struct { host string @@ -130,3 +153,187 @@ func TestGatewayGet(t *testing.T) { } } } + +func TestIPNSHostnameRedirect(t *testing.T) { + ns := mockNamesys{} + ts, n := newTestServerAndNode(t, ns) + t.Logf("test server url: %s", ts.URL) + defer ts.Close() + + // create /ipns/example.net/foo/index.html + _, dagn1, err := coreunix.AddWrapped(n, strings.NewReader("_"), "_") + if err != nil { + t.Fatal(err) + } + _, dagn2, err := coreunix.AddWrapped(n, strings.NewReader("_"), "index.html") + if err != nil { + t.Fatal(err) + } + dagn1.AddNodeLink("foo", dagn2) + if err != nil { + t.Fatal(err) + } + + err = n.DAG.AddRecursive(dagn1) + if err != nil { + t.Fatal(err) + } + + k, err := dagn1.Key() + if err != nil { + t.Fatal(err) + } + t.Logf("k: %s\n", k) + ns["/ipns/example.net"] = path.FromString("/ipfs/" + k.String()) + + // make request to directory containing index.html + req, err := http.NewRequest("GET", ts.URL+"/foo", nil) + if err != nil { + t.Fatal(err) + } + req.Host = "example.net" + + res, err := doWithoutRedirect(req) + if err != nil { + t.Fatal(err) + } + + // expect 302 redirect to same path, but with trailing slash + if res.StatusCode != 302 { + t.Errorf("status is %d, expected 302", res.StatusCode) + } + hdr := res.Header["Location"] + if len(hdr) < 1 { + t.Errorf("location header not present") + } else if hdr[0] != "/foo/" { + t.Errorf("location header is %v, expected /foo/", hdr[0]) + } +} + +func TestIPNSHostnameBacklinks(t *testing.T) { + ns := mockNamesys{} + ts, n := newTestServerAndNode(t, ns) + t.Logf("test server url: %s", ts.URL) + defer ts.Close() + + // create /ipns/example.net/foo/ + _, dagn1, err := coreunix.AddWrapped(n, strings.NewReader("1"), "file.txt") + if err != nil { + t.Fatal(err) + } + _, dagn2, err := coreunix.AddWrapped(n, strings.NewReader("2"), "file.txt") + if err != nil { + t.Fatal(err) + } + _, dagn3, err := coreunix.AddWrapped(n, strings.NewReader("3"), "file.txt") + if err != nil { + t.Fatal(err) + } + dagn2.AddNodeLink("bar", dagn3) + dagn1.AddNodeLink("foo", dagn2) + if err != nil { + t.Fatal(err) + } + + err = n.DAG.AddRecursive(dagn1) + if err != nil { + t.Fatal(err) + } + + k, err := dagn1.Key() + if err != nil { + t.Fatal(err) + } + t.Logf("k: %s\n", k) + ns["/ipns/example.net"] = path.FromString("/ipfs/" + k.String()) + + // make request to directory listing + req, err := http.NewRequest("GET", ts.URL+"/foo/", nil) + if err != nil { + t.Fatal(err) + } + req.Host = "example.net" + + res, err := doWithoutRedirect(req) + if err != nil { + t.Fatal(err) + } + + // expect correct backlinks + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("error reading response: %s", err) + } + s := string(body) + t.Logf("body: %s\n", string(body)) + + if !strings.Contains(s, "Index of /foo/") { + t.Fatalf("expected a path in directory listing") + } + if !strings.Contains(s, "") { + t.Fatalf("expected backlink in directory listing") + } + if !strings.Contains(s, "") { + t.Fatalf("expected file in directory listing") + } + + // make request to directory listing + req, err = http.NewRequest("GET", ts.URL, nil) + if err != nil { + t.Fatal(err) + } + req.Host = "example.net" + + res, err = doWithoutRedirect(req) + if err != nil { + t.Fatal(err) + } + + // expect correct backlinks + body, err = ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("error reading response: %s", err) + } + s = string(body) + t.Logf("body: %s\n", string(body)) + + if !strings.Contains(s, "Index of /") { + t.Fatalf("expected a path in directory listing") + } + if !strings.Contains(s, "") { + t.Fatalf("expected backlink in directory listing") + } + if !strings.Contains(s, "") { + t.Fatalf("expected file in directory listing") + } + + // make request to directory listing + req, err = http.NewRequest("GET", ts.URL+"/foo/bar/", nil) + if err != nil { + t.Fatal(err) + } + req.Host = "example.net" + + res, err = doWithoutRedirect(req) + if err != nil { + t.Fatal(err) + } + + // expect correct backlinks + body, err = ioutil.ReadAll(res.Body) + if err != nil { + t.Fatalf("error reading response: %s", err) + } + s = string(body) + t.Logf("body: %s\n", string(body)) + + if !strings.Contains(s, "Index of /foo/bar/") { + t.Fatalf("expected a path in directory listing") + } + if !strings.Contains(s, "") { + t.Fatalf("expected backlink in directory listing") + } + if !strings.Contains(s, "") { + t.Fatalf("expected file in directory listing") + } +} diff --git a/core/corehttp/ipns_hostname.go b/core/corehttp/ipns_hostname.go index 10edb0ace..94faccd5d 100644 --- a/core/corehttp/ipns_hostname.go +++ b/core/corehttp/ipns_hostname.go @@ -24,6 +24,7 @@ func IPNSHostnameOption() ServeOption { if len(host) > 0 && isd.IsDomain(host) { name := "/ipns/" + host if _, err := n.Namesys.Resolve(ctx, name); err == nil { + r.Header["X-IPNS-Original-Path"] = []string{r.URL.Path} r.URL.Path = name + r.URL.Path } }