From a3a10a4bc11cb268bb06cabc81d0f860493e161f Mon Sep 17 00:00:00 2001 From: Lars Gierth Date: Sun, 16 Aug 2015 00:52:59 +0200 Subject: [PATCH 1/2] gateway: bring back TestGatewayGet test License: MIT Signed-off-by: Lars Gierth --- core/corehttp/gateway_test.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/core/corehttp/gateway_test.go b/core/corehttp/gateway_test.go index 7ad3584da57..09415c712a0 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 { @@ -64,14 +64,19 @@ func (dh *delegatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func TestGatewayGet(t *testing.T) { - t.Skip("not sure whats going on here") + // mock node and namesys ns := mockNamesys{} - n := newNodeWithMockNamesys(t, ns) + n, err := newNodeWithMockNamesys(ns) + if err != nil { + t.Fatal(err) + } + + // mock ipfs object k, err := coreunix.Add(n, strings.NewReader("fnord")) if err != nil { t.Fatal(err) } - ns["example.com"] = path.FromString("/ipfs/" + k) + ns["/ipns/example.com"] = path.FromString("/ipfs/" + k) // need this variable here since we need to construct handler with // listener, and server with handler. yay cycles. From 09d750172473a7991bca0b0dad9529a5bdbda2fb Mon Sep 17 00:00:00 2001 From: Lars Gierth Date: Sat, 15 Aug 2015 01:41:48 +0200 Subject: [PATCH 2/2] gateway: make IPNSHostname complete IPNSHostnameOption() touches the URL path only on the way in, but not on the way out. This commit makes it complete by touching the following URLs in responses: - Heading, file links, back links in directory listings - Redirecting /foo to /foo/ if there's an index.html link - Omit Suborigin header License: MIT Signed-off-by: Lars Gierth --- core/corehttp/gateway_handler.go | 55 ++++++-- core/corehttp/gateway_test.go | 222 +++++++++++++++++++++++++++++-- core/corehttp/ipns_hostname.go | 1 + 3 files changed, 258 insertions(+), 20 deletions(-) diff --git a/core/corehttp/gateway_handler.go b/core/corehttp/gateway_handler.go index 55094367564..154db3b5307 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 09415c712a0..a90bd641e1a 100644 --- a/core/corehttp/gateway_test.go +++ b/core/corehttp/gateway_test.go @@ -63,26 +63,30 @@ func (dh *delegatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { dh.Handler.ServeHTTP(w, r) } -func TestGatewayGet(t *testing.T) { - // mock node and namesys - ns := mockNamesys{} - n, err := newNodeWithMockNamesys(ns) - if err != nil { - t.Fatal(err) +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 +} - // mock ipfs object - k, err := coreunix.Add(n, strings.NewReader("fnord")) +func newTestServerAndNode(t *testing.T, ns mockNamesys) (*httptest.Server, *core.IpfsNode) { + n, err := newNodeWithMockNamesys(ns) if err != nil { t.Fatal(err) } - ns["/ipns/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, @@ -93,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 @@ -135,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 10edb0aced3..94faccd5db3 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 } }