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:
@ -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)
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user