1
0
mirror of https://github.com/ipfs/kubo.git synced 2025-06-28 00:39:31 +08:00

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 <larsg@systemli.org>
This commit is contained in:
Lars Gierth
2015-08-15 01:41:48 +02:00
parent a3a10a4bc1
commit 09d7501724
3 changed files with 259 additions and 21 deletions

View File

@ -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)

View File

@ -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{}
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)
}
// mock ipfs object
k, err := coreunix.Add(n, strings.NewReader("fnord"))
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, "<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")
}
}

View File

@ -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
}
}