1
0
mirror of https://github.com/ipfs/kubo.git synced 2025-06-26 23:53:19 +08:00

Merge pull request #1577 from lgierth/gateway-host-header

gateway: make IPNSHostname work on responses too
This commit is contained in:
Juan Benet
2015-08-19 05:50:05 +02:00
3 changed files with 263 additions and 20 deletions

View File

@ -90,6 +90,19 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
urlPath := r.URL.Path 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) { if i.config.BlockList != nil && i.config.BlockList.ShouldBlock(urlPath) {
w.WriteHeader(http.StatusForbidden) w.WriteHeader(http.StatusForbidden)
w.Write([]byte("403 - Forbidden")) 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) w.Header().Set("X-IPFS-Path", urlPath)
// Suborigin header, sandboxes apps from each other in the browser (even // Suborigin header, sandboxes apps from each other in the browser (even
// though they are served from the same gateway domain). NOTE: This is not // though they are served from the same gateway domain).
// yet widely supported by browsers. //
pathRoot := strings.SplitN(urlPath, "/", 4)[2] // Omited if the path was treated by IPNSHostnameOption(), for example
w.Header().Set("Suborigin", pathRoot) // 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) dr, err := uio.NewDagReader(ctx, nd, i.node.DAG)
if err != nil && err != uio.ErrIsDir { if err != nil && err != uio.ErrIsDir {
@ -150,13 +170,16 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
foundIndex := false foundIndex := false
for _, link := range nd.Links { for _, link := range nd.Links {
if link.Name == "index.html" { if link.Name == "index.html" {
log.Debugf("found index.html link for %s", urlPath)
foundIndex = true
if urlPath[len(urlPath)-1] != '/' { 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 return
} }
log.Debug("found index")
foundIndex = true
// return index page instead. // return index page instead.
nd, err := core.Resolve(ctx, i.node, path.Path(urlPath+"/index.html")) nd, err := core.Resolve(ctx, i.node, path.Path(urlPath+"/index.html"))
if err != nil { if err != nil {
@ -177,7 +200,8 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
break 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) dirListing = append(dirListing, di)
} }
@ -185,7 +209,7 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
if r.Method != "HEAD" { if r.Method != "HEAD" {
// construct the correct back link // construct the correct back link
// https://github.com/ipfs/go-ipfs/issues/1365 // 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/ // don't go further up than /ipfs/$hash/
pathSplit := strings.Split(backLink, "/") 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{ tplData := listingTemplateData{
Listing: dirListing, Listing: dirListing,
Path: urlPath, Path: originalUrlPath,
BackLink: backLink, BackLink: backLink,
} }
err := listingTemplate.Execute(w, tplData) err := listingTemplate.Execute(w, tplData)

View File

@ -37,7 +37,7 @@ func (m mockNamesys) Publish(ctx context.Context, name ci.PrivKey, value path.Pa
return errors.New("not implemented for mockNamesys") 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{ c := config.Config{
Identity: config.Identity{ Identity: config.Identity{
PeerID: "Qmfoo", // required by offline node 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)) n, err := core.NewIPFSNode(context.Background(), core.Offline(r))
if err != nil { if err != nil {
t.Fatal(err) return nil, err
} }
n.Namesys = ns n.Namesys = ns
return n return n, nil
} }
type delegatedHandler struct { type delegatedHandler struct {
@ -63,21 +63,30 @@ func (dh *delegatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
dh.Handler.ServeHTTP(w, r) dh.Handler.ServeHTTP(w, r)
} }
func TestGatewayGet(t *testing.T) { func doWithoutRedirect(req *http.Request) (*http.Response, error) {
t.Skip("not sure whats going on here") tag := "without-redirect"
ns := mockNamesys{} c := &http.Client{
n := newNodeWithMockNamesys(t, ns) CheckRedirect: func(req *http.Request, via []*http.Request) error {
k, err := coreunix.Add(n, strings.NewReader("fnord")) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
ns["example.com"] = path.FromString("/ipfs/" + k)
// need this variable here since we need to construct handler with // need this variable here since we need to construct handler with
// listener, and server with handler. yay cycles. // listener, and server with handler. yay cycles.
dh := &delegatedHandler{} dh := &delegatedHandler{}
ts := httptest.NewServer(dh) ts := httptest.NewServer(dh)
defer ts.Close()
dh.Handler, err = makeHandler(n, dh.Handler, err = makeHandler(n,
ts.Listener, ts.Listener,
@ -88,6 +97,20 @@ func TestGatewayGet(t *testing.T) {
t.Fatal(err) 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) t.Log(ts.URL)
for _, test := range []struct { for _, test := range []struct {
host string host string
@ -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")
}
}

View File

@ -24,6 +24,7 @@ func IPNSHostnameOption() ServeOption {
if len(host) > 0 && isd.IsDomain(host) { if len(host) > 0 && isd.IsDomain(host) {
name := "/ipns/" + host name := "/ipns/" + host
if _, err := n.Namesys.Resolve(ctx, name); err == nil { 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 r.URL.Path = name + r.URL.Path
} }
} }