Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gateway: make IPNSHostname work on responses too #1577

Merged
merged 2 commits into from Aug 19, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 45 additions & 10 deletions core/corehttp/gateway_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe want to set here: ipnsHostRedirection = true and use that for the conditions below?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I introduced a variable in order to avoid the piggybacking. I also fixed backlink edge-cases and added more tests for them.

It should be good to go now.

ipnsHostname = true
}

if i.config.BlockList != nil && i.config.BlockList.ShouldBlock(urlPath) {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("403 - Forbidden"))
Expand All @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it should be corrected then? instead of omitted?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With IPNSHostname the gateway serves one object per hostname, while otherwise it serves all the objects that exist (through /ipfs and /ipns). I thought there'd be no need to separate it from anything, while under /ipfs and /ipns, each object and identity should be separated.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohhhhh that makes sense.

//
// 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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -177,15 +200,16 @@ 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)
}

if !foundIndex {
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, "/")
Expand All @@ -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)
Expand Down
227 changes: 217 additions & 10 deletions core/corehttp/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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, "<a href=\"/\">") {
t.Fatalf("expected backlink in directory listing")
}
if !strings.Contains(s, "<a href=\"/foo/file.txt\">") {
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, "<a href=\"/\">") {
t.Fatalf("expected backlink in directory listing")
}
if !strings.Contains(s, "<a href=\"/file.txt\">") {
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, "<a href=\"/foo/\">") {
t.Fatalf("expected backlink in directory listing")
}
if !strings.Contains(s, "<a href=\"/foo/bar/file.txt\">") {
t.Fatalf("expected file in directory listing")
}
}
1 change: 1 addition & 0 deletions core/corehttp/ipns_hostname.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down