kubo/core/corehttp/gateway_test.go
Jack Loughran 9ef1486307 show hash if not in original url
Change the template handler to pass the hash to the directory listing template if the original url doesn't contain the hash. The directory listing template will display the hash in grey under the original url if the hash is passed to it.

License: MIT
Signed-off-by: Jack Loughran <j@ckloughran.com>
2019-01-05 11:16:17 -08:00

637 lines
17 KiB
Go

package corehttp
import (
"context"
"errors"
"io/ioutil"
"math"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
version "github.com/ipfs/go-ipfs"
core "github.com/ipfs/go-ipfs/core"
coreunix "github.com/ipfs/go-ipfs/core/coreunix"
namesys "github.com/ipfs/go-ipfs/namesys"
nsopts "github.com/ipfs/go-ipfs/namesys/opts"
repo "github.com/ipfs/go-ipfs/repo"
ci "gx/ipfs/QmNiJiXwWE3kRhZrC5ej3kSjWHm337pYfhjLGSCDNKJP2s/go-libp2p-crypto"
id "gx/ipfs/QmRBaUEQEeFWywfrZJ64QgsmvcqgLSK3VbvGMR2NM2Edpf/go-libp2p/p2p/protocol/identify"
path "gx/ipfs/QmZErC2Ay6WuGi96CPg316PwitdwgLo6RxZRqVjJjRj2MR/go-path"
config "gx/ipfs/QmcZfkbgwwwH5ZLTQRHkSQBDiDqd3skY2eU6MZRgWuXcse/go-ipfs-config"
dag "gx/ipfs/QmdV35UHnL1FM52baPkeUo6u7Fxm2CRUkPTLRPxeF8a4Ap/go-merkledag"
datastore "gx/ipfs/Qmf4xQhNomPNhrtZc67qSnfJSjxjXs9LWvknJtSXwimPrM/go-datastore"
syncds "gx/ipfs/Qmf4xQhNomPNhrtZc67qSnfJSjxjXs9LWvknJtSXwimPrM/go-datastore/sync"
)
// `ipfs object new unixfs-dir`
var emptyDir = "/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn"
type mockNamesys map[string]path.Path
func (m mockNamesys) Resolve(ctx context.Context, name string, opts ...nsopts.ResolveOpt) (value path.Path, err error) {
cfg := nsopts.DefaultResolveOpts()
for _, o := range opts {
o(&cfg)
}
depth := cfg.Depth
if depth == nsopts.UnlimitedDepth {
depth = math.MaxUint64
}
for strings.HasPrefix(name, "/ipns/") {
if depth <= 0 {
return value, namesys.ErrResolveRecursion
}
depth--
var ok bool
value, ok = m[name]
if !ok {
return "", namesys.ErrResolveFailed
}
name = value.String()
}
return value, nil
}
func (m mockNamesys) ResolveAsync(ctx context.Context, name string, opts ...nsopts.ResolveOpt) <-chan namesys.Result {
out := make(chan namesys.Result, 1)
v, err := m.Resolve(ctx, name, opts...)
out <- namesys.Result{Path: v, Err: err}
close(out)
return out
}
func (m mockNamesys) Publish(ctx context.Context, name ci.PrivKey, value path.Path) error {
return errors.New("not implemented for mockNamesys")
}
func (m mockNamesys) PublishWithEOL(ctx context.Context, name ci.PrivKey, value path.Path, _ time.Time) error {
return errors.New("not implemented for mockNamesys")
}
func (m mockNamesys) GetResolver(subs string) (namesys.Resolver, bool) {
return nil, false
}
func newNodeWithMockNamesys(ns mockNamesys) (*core.IpfsNode, error) {
c := config.Config{
Identity: config.Identity{
PeerID: "QmTFauExutTsy4XP6JbMFcw2Wa9645HJt2bTqL6qYDCKfe", // required by offline node
},
}
r := &repo.Mock{
C: c,
D: syncds.MutexWrap(datastore.NewMapDatastore()),
}
n, err := core.NewNode(context.Background(), &core.BuildCfg{Repo: r})
if err != nil {
return nil, err
}
n.Namesys = ns
return n, nil
}
type delegatedHandler struct {
http.Handler
}
func (dh *delegatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
dh.Handler.ServeHTTP(w, r)
}
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)
}
cfg, err := n.Repo.Config()
if err != nil {
t.Fatal(err)
}
cfg.Gateway.PathPrefixes = []string{"/good-prefix"}
// need this variable here since we need to construct handler with
// listener, and server with handler. yay cycles.
dh := &delegatedHandler{}
ts := httptest.NewServer(dh)
dh.Handler, err = makeHandler(n,
ts.Listener,
IPNSHostnameOption(),
GatewayOption(false, "/ipfs", "/ipns"),
VersionOption(),
)
if err != nil {
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)
ns["/ipns/working.example.com"] = path.FromString("/ipfs/" + k)
ns["/ipns/double.example.com"] = path.FromString("/ipns/working.example.com")
ns["/ipns/triple.example.com"] = path.FromString("/ipns/double.example.com")
ns["/ipns/broken.example.com"] = path.FromString("/ipns/" + k)
// We picked .man because:
// 1. It's a valid TLD.
// 2. Go treats it as the file extension for "man" files (even though
// nobody actually *uses* this extension, AFAIK).
//
// Unfortunately, this may not work on all platforms as file type
// detection is platform dependent.
ns["/ipns/example.man"] = path.FromString("/ipfs/" + k)
t.Log(ts.URL)
for _, test := range []struct {
host string
path string
status int
text string
}{
{"localhost:5001", "/", http.StatusNotFound, "404 page not found\n"},
{"localhost:5001", "/" + k, http.StatusNotFound, "404 page not found\n"},
{"localhost:5001", "/ipfs/" + k, http.StatusOK, "fnord"},
{"localhost:5001", "/ipns/nxdomain.example.com", http.StatusNotFound, "ipfs resolve -r /ipns/nxdomain.example.com: " + namesys.ErrResolveFailed.Error() + "\n"},
{"localhost:5001", "/ipns/%0D%0A%0D%0Ahello", http.StatusNotFound, "ipfs resolve -r /ipns/%0D%0A%0D%0Ahello: " + namesys.ErrResolveFailed.Error() + "\n"},
{"localhost:5001", "/ipns/example.com", http.StatusOK, "fnord"},
{"example.com", "/", http.StatusOK, "fnord"},
{"working.example.com", "/", http.StatusOK, "fnord"},
{"double.example.com", "/", http.StatusOK, "fnord"},
{"triple.example.com", "/", http.StatusOK, "fnord"},
{"working.example.com", "/ipfs/" + k, http.StatusNotFound, "ipfs resolve -r /ipns/working.example.com/ipfs/" + k + ": no link named \"ipfs\" under " + k + "\n"},
{"broken.example.com", "/", http.StatusNotFound, "ipfs resolve -r /ipns/broken.example.com/: " + namesys.ErrResolveFailed.Error() + "\n"},
{"broken.example.com", "/ipfs/" + k, http.StatusNotFound, "ipfs resolve -r /ipns/broken.example.com/ipfs/" + k + ": " + namesys.ErrResolveFailed.Error() + "\n"},
// This test case ensures we don't treat the TLD as a file extension.
{"example.man", "/", http.StatusOK, "fnord"},
} {
var c http.Client
r, err := http.NewRequest("GET", ts.URL+test.path, nil)
if err != nil {
t.Fatal(err)
}
r.Host = test.host
resp, err := c.Do(r)
urlstr := "http://" + test.host + test.path
if err != nil {
t.Errorf("error requesting %s: %s", urlstr, err)
continue
}
defer resp.Body.Close()
contentType := resp.Header.Get("Content-Type")
if contentType != "text/plain; charset=utf-8" {
t.Errorf("expected content type to be text/plain, got %s", contentType)
}
if resp.StatusCode != test.status {
t.Errorf("got %d, expected %d from %s", resp.StatusCode, test.status, urlstr)
continue
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("error reading response from %s: %s", urlstr, err)
}
if string(body) != test.text {
t.Errorf("unexpected response body from %s: expected %q; got %q", urlstr, test.text, body)
continue
}
}
}
func TestIPNSHostnameRedirect(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
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.(*dag.ProtoNode).AddNodeLink("foo", dagn2)
if err != nil {
t.Fatal(err)
}
err = n.DAG.Add(ctx, dagn2)
if err != nil {
t.Fatal(err)
}
err = n.DAG.Add(ctx, dagn1)
if err != nil {
t.Fatal(err)
}
k := dagn1.Cid()
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])
}
// make request with prefix to directory containing index.html
req, err = http.NewRequest("GET", ts.URL+"/foo", nil)
if err != nil {
t.Fatal(err)
}
req.Host = "example.net"
req.Header.Set("X-Ipfs-Gateway-Prefix", "/good-prefix")
res, err = doWithoutRedirect(req)
if err != nil {
t.Fatal(err)
}
// expect 302 redirect to same path, but with prefix and 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] != "/good-prefix/foo/" {
t.Errorf("location header is %v, expected /good-prefix/foo/", hdr[0])
}
// make sure /version isn't exposed
req, err = http.NewRequest("GET", ts.URL+"/version", nil)
if err != nil {
t.Fatal(err)
}
req.Host = "example.net"
req.Header.Set("X-Ipfs-Gateway-Prefix", "/good-prefix")
res, err = doWithoutRedirect(req)
if err != nil {
t.Fatal(err)
}
if res.StatusCode != 404 {
t.Fatalf("expected a 404 error, got: %s", res.Status)
}
}
func TestIPNSHostnameBacklinks(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
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.(*dag.ProtoNode).AddNodeLink("bar", dagn3)
dagn1.(*dag.ProtoNode).AddNodeLink("foo? #<'", dagn2)
if err != nil {
t.Fatal(err)
}
err = n.DAG.Add(ctx, dagn3)
if err != nil {
t.Fatal(err)
}
err = n.DAG.Add(ctx, dagn2)
if err != nil {
t.Fatal(err)
}
err = n.DAG.Add(ctx, dagn1)
if err != nil {
t.Fatal(err)
}
k := dagn1.Cid()
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%3F%20%23%3C%27/", 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? #&lt;&#39;/") {
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%3F%20%23%3C%27/file.txt\">") {
t.Fatalf("expected file in directory listing")
}
if !strings.Contains(s, dagn2.Cid().String()) {
t.Fatalf("expected hash in directory listing")
}
// make request to directory listing at root
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 at root
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")
}
if !strings.Contains(s, dagn1.Cid().String()) {
t.Fatalf("expected hash in directory listing")
}
// make request to directory listing
req, err = http.NewRequest("GET", ts.URL+"/foo%3F%20%23%3C%27/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? #&lt;&#39;/bar/") {
t.Fatalf("expected a path in directory listing")
}
if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/\">") {
t.Fatalf("expected backlink in directory listing")
}
if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/bar/file.txt\">") {
t.Fatalf("expected file in directory listing")
}
if !strings.Contains(s, dagn3.Cid().String()) {
t.Fatalf("expected hash in directory listing")
}
// make request to directory listing with prefix
req, err = http.NewRequest("GET", ts.URL, nil)
if err != nil {
t.Fatal(err)
}
req.Host = "example.net"
req.Header.Set("X-Ipfs-Gateway-Prefix", "/good-prefix")
res, err = doWithoutRedirect(req)
if err != nil {
t.Fatal(err)
}
// expect correct backlinks with prefix
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 /good-prefix") {
t.Fatalf("expected a path in directory listing")
}
if !strings.Contains(s, "<a href=\"/good-prefix/\">") {
t.Fatalf("expected backlink in directory listing")
}
if !strings.Contains(s, "<a href=\"/good-prefix/file.txt\">") {
t.Fatalf("expected file in directory listing")
}
if !strings.Contains(s, dagn1.Cid().String()) {
t.Fatalf("expected hash in directory listing")
}
// make request to directory listing with illegal prefix
req, err = http.NewRequest("GET", ts.URL, nil)
if err != nil {
t.Fatal(err)
}
req.Host = "example.net"
req.Header.Set("X-Ipfs-Gateway-Prefix", "/bad-prefix")
// make request to directory listing with evil prefix
req, err = http.NewRequest("GET", ts.URL, nil)
if err != nil {
t.Fatal(err)
}
req.Host = "example.net"
req.Header.Set("X-Ipfs-Gateway-Prefix", "//good-prefix/foo")
res, err = doWithoutRedirect(req)
if err != nil {
t.Fatal(err)
}
// expect correct backlinks without illegal prefix
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")
}
if !strings.Contains(s, dagn1.Cid().String()) {
t.Fatalf("expected hash in directory listing")
}
}
func TestCacheControlImmutable(t *testing.T) {
ts, _ := newTestServerAndNode(t, nil)
t.Logf("test server url: %s", ts.URL)
defer ts.Close()
req, err := http.NewRequest("GET", ts.URL+emptyDir+"/", nil)
if err != nil {
t.Fatal(err)
}
res, err := doWithoutRedirect(req)
if err != nil {
t.Fatal(err)
}
// check the immutable tag isn't set
hdrs, ok := res.Header["Cache-Control"]
if ok {
for _, hdr := range hdrs {
if strings.Contains(hdr, "immutable") {
t.Fatalf("unexpected Cache-Control: immutable on directory listing: %s", hdr)
}
}
}
}
func TestGoGetSupport(t *testing.T) {
ts, _ := newTestServerAndNode(t, nil)
t.Logf("test server url: %s", ts.URL)
defer ts.Close()
// mimic go-get
req, err := http.NewRequest("GET", ts.URL+emptyDir+"?go-get=1", nil)
if err != nil {
t.Fatal(err)
}
res, err := doWithoutRedirect(req)
if err != nil {
t.Fatal(err)
}
if res.StatusCode != 200 {
t.Errorf("status is %d, expected 200", res.StatusCode)
}
}
func TestVersion(t *testing.T) {
version.CurrentCommit = "theshortcommithash"
ns := mockNamesys{}
ts, _ := newTestServerAndNode(t, ns)
t.Logf("test server url: %s", ts.URL)
defer ts.Close()
req, err := http.NewRequest("GET", ts.URL+"/version", nil)
if err != nil {
t.Fatal(err)
}
res, err := doWithoutRedirect(req)
if err != nil {
t.Fatal(err)
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Fatalf("error reading response: %s", err)
}
s := string(body)
if !strings.Contains(s, "Commit: theshortcommithash") {
t.Fatalf("response doesn't contain commit:\n%s", s)
}
if !strings.Contains(s, "Client Version: "+id.ClientVersion) {
t.Fatalf("response doesn't contain client version:\n%s", s)
}
if !strings.Contains(s, "Protocol Version: "+id.LibP2PVersion) {
t.Fatalf("response doesn't contain protocol version:\n%s", s)
}
}