Skip to content

Commit

Permalink
Merge pull request #1577 from lgierth/gateway-host-header
Browse files Browse the repository at this point in the history
gateway: make IPNSHostname work on responses too
  • Loading branch information
jbenet committed Aug 19, 2015
2 parents d1dd53b + 09d7501 commit 3dfe02a
Show file tree
Hide file tree
Showing 3 changed files with 263 additions and 20 deletions.
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]
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.
//
// 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

0 comments on commit 3dfe02a

Please sign in to comment.