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