Skip to content

Commit

Permalink
Merge pull request ipfs/kubo#7366 from ipfs/release-v0.6.0
Browse files Browse the repository at this point in the history
Release v0.6.0

This commit was moved from ipfs/kubo@d6e036a
  • Loading branch information
aschmahmann authored Jun 19, 2020
2 parents daface2 + ff0c91e commit c5ee923
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 28 deletions.
139 changes: 117 additions & 22 deletions gateway/core/corehttp/gateway_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import (
gopath "path"
"regexp"
"runtime/debug"
"strconv"
"strings"
"time"

humanize "github.com/dustin/go-humanize"
"github.com/gabriel-vasile/mimetype"
"github.com/ipfs/go-cid"
files "github.com/ipfs/go-ipfs-files"
assets "github.com/ipfs/go-ipfs/assets"
dag "github.com/ipfs/go-merkledag"
mfs "github.com/ipfs/go-mfs"
path "github.com/ipfs/go-path"
Expand Down Expand Up @@ -202,6 +205,10 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
webError(w, "ipfs resolve -r "+escapedURLPath, err, http.StatusServiceUnavailable)
return
default:
if i.servePretty404IfPresent(w, r, parsedPath) {
return
}

webError(w, "ipfs resolve -r "+escapedURLPath, err, http.StatusNotFound)
return
}
Expand All @@ -216,16 +223,26 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request

defer dr.Close()

// Check etag send back to us
etag := "\"" + resolvedPath.Cid().String() + "\""
if r.Header.Get("If-None-Match") == etag || r.Header.Get("If-None-Match") == "W/"+etag {
var responseEtag string

// we need to figure out whether this is a directory before doing most of the heavy lifting below
_, ok := dr.(files.Directory)

if ok && assets.BindataVersionHash != "" {
responseEtag = `"DirIndex-` + assets.BindataVersionHash + `_CID-` + resolvedPath.Cid().String() + `"`
} else {
responseEtag = `"` + resolvedPath.Cid().String() + `"`
}

// Check etag sent back to us
if r.Header.Get("If-None-Match") == responseEtag || r.Header.Get("If-None-Match") == `W/`+responseEtag {
w.WriteHeader(http.StatusNotModified)
return
}

i.addUserHeaders(w) // ok, _now_ write user's headers.
w.Header().Set("X-IPFS-Path", urlPath)
w.Header().Set("Etag", etag)
w.Header().Set("Etag", responseEtag)

// set these headers _after_ the error, for we may just not have it
// and don't want the client to cache a 500 response...
Expand Down Expand Up @@ -285,6 +302,18 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
return
}

// See statusResponseWriter.WriteHeader
// and https://github.com/ipfs/go-ipfs/issues/7164
// Note: this needs to occur before listingTemplate.Execute otherwise we get
// superfluous response.WriteHeader call from prometheus/client_golang
if w.Header().Get("Location") != "" {
w.WriteHeader(http.StatusMovedPermanently)
return
}

// A HTML directory index will be presented, be sure to set the correct
// type instead of relying on autodetection (which may fail).
w.Header().Set("Content-Type", "text/html")
if r.Method == http.MethodHead {
return
}
Expand Down Expand Up @@ -330,28 +359,16 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
}
}

var hash string
if !strings.HasPrefix(urlPath, ipfsPathPrefix) {
hash = resolvedPath.Cid().String()
}
hash := resolvedPath.Cid().String()

// See comment above where originalUrlPath is declared.
tplData := listingTemplateData{
Listing: dirListing,
Path: originalUrlPath,
Path: urlPath,
BackLink: backLink,
Hash: hash,
}

// See statusResponseWriter.WriteHeader
// and https://github.com/ipfs/go-ipfs/issues/7164
// Note: this needs to occur before listingTemplate.Execute otherwise we get
// superfluous response.WriteHeader call from prometheus/client_golang
if w.Header().Get("Location") != "" {
w.WriteHeader(http.StatusMovedPermanently)
return
}

err = listingTemplate.Execute(w, tplData)
if err != nil {
internalWebError(w, err)
Expand Down Expand Up @@ -379,10 +396,16 @@ func (i *gatewayHandler) serveFile(w http.ResponseWriter, req *http.Request, nam
} else {
ctype = mime.TypeByExtension(gopath.Ext(name))
if ctype == "" {
buf := make([]byte, 512)
n, _ := io.ReadFull(content, buf[:])
ctype = http.DetectContentType(buf[:n])
_, err := content.Seek(0, io.SeekStart)
// uses https://github.com/gabriel-vasile/mimetype library to determine the content type.
// Fixes https://github.com/ipfs/go-ipfs/issues/7252
mimeType, err := mimetype.DetectReader(content)
if err != nil {
http.Error(w, fmt.Sprintf("cannot detect content-type: %s", err.Error()), http.StatusInternalServerError)
return
}

ctype = mimeType.String()
_, err = content.Seek(0, io.SeekStart)
if err != nil {
http.Error(w, "seeker can't seek", http.StatusInternalServerError)
return
Expand All @@ -402,6 +425,36 @@ func (i *gatewayHandler) serveFile(w http.ResponseWriter, req *http.Request, nam
http.ServeContent(w, req, name, modtime, content)
}

func (i *gatewayHandler) servePretty404IfPresent(w http.ResponseWriter, r *http.Request, parsedPath ipath.Path) bool {
resolved404Path, ctype, err := i.searchUpTreeFor404(r, parsedPath)
if err != nil {
return false
}

dr, err := i.api.Unixfs().Get(r.Context(), resolved404Path)
if err != nil {
return false
}
defer dr.Close()

f, ok := dr.(files.File)
if !ok {
return false
}

size, err := f.Size()
if err != nil {
return false
}

log.Debugf("using pretty 404 file for %s", parsedPath.String())
w.Header().Set("Content-Type", ctype)
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
w.WriteHeader(http.StatusNotFound)
_, err = io.CopyN(w, f, size)
return err == nil
}

func (i *gatewayHandler) postHandler(w http.ResponseWriter, r *http.Request) {
p, err := i.api.Unixfs().Add(r.Context(), files.NewReaderFile(r.Body))
if err != nil {
Expand Down Expand Up @@ -615,3 +668,45 @@ func getFilename(s string) string {
}
return gopath.Base(s)
}

func (i *gatewayHandler) searchUpTreeFor404(r *http.Request, parsedPath ipath.Path) (ipath.Resolved, string, error) {
filename404, ctype, err := preferred404Filename(r.Header.Values("Accept"))
if err != nil {
return nil, "", err
}

pathComponents := strings.Split(parsedPath.String(), "/")

for idx := len(pathComponents); idx >= 3; idx-- {
pretty404 := gopath.Join(append(pathComponents[0:idx], filename404)...)
parsed404Path := ipath.New("/" + pretty404)
if parsed404Path.IsValid() != nil {
break
}
resolvedPath, err := i.api.ResolvePath(r.Context(), parsed404Path)
if err != nil {
continue
}
return resolvedPath, ctype, nil
}

return nil, "", fmt.Errorf("no pretty 404 in any parent folder")
}

func preferred404Filename(acceptHeaders []string) (string, string, error) {
// If we ever want to offer a 404 file for a different content type
// then this function will need to parse q weightings, but for now
// the presence of anything matching HTML is enough.
for _, acceptHeader := range acceptHeaders {
accepted := strings.Split(acceptHeader, ",")
for _, spec := range accepted {
contentType := strings.SplitN(spec, ";", 1)[0]
switch contentType {
case "*/*", "text/*", "text/html":
return "ipfs-404.html", "text/html", nil
}
}
}

return "", "", fmt.Errorf("there is no 404 file for the requested content types")
}
70 changes: 67 additions & 3 deletions gateway/core/corehttp/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,70 @@ func TestGatewayGet(t *testing.T) {
}
}

func TestPretty404(t *testing.T) {
ns := mockNamesys{}
ts, api, ctx := newTestServerAndNode(t, ns)
defer ts.Close()

f1 := files.NewMapDirectory(map[string]files.Node{
"ipfs-404.html": files.NewBytesFile([]byte("Custom 404")),
"deeper": files.NewMapDirectory(map[string]files.Node{
"ipfs-404.html": files.NewBytesFile([]byte("Deep custom 404")),
}),
})

k, err := api.Unixfs().Add(ctx, f1)
if err != nil {
t.Fatal(err)
}

host := "example.net"
ns["/ipns/"+host] = path.FromString(k.String())

for _, test := range []struct {
path string
accept string
status int
text string
}{
{"/ipfs-404.html", "text/html", http.StatusOK, "Custom 404"},
{"/nope", "text/html", http.StatusNotFound, "Custom 404"},
{"/nope", "text/*", http.StatusNotFound, "Custom 404"},
{"/nope", "*/*", http.StatusNotFound, "Custom 404"},
{"/nope", "application/json", http.StatusNotFound, "ipfs resolve -r /ipns/example.net/nope: no link named \"nope\" under QmcmnF7XG5G34RdqYErYDwCKNFQ6jb8oKVR21WAJgubiaj\n"},
{"/deeper/nope", "text/html", http.StatusNotFound, "Deep custom 404"},
{"/deeper/", "text/html", http.StatusOK, ""},
{"/deeper", "text/html", http.StatusOK, ""},
{"/nope/nope", "text/html", http.StatusNotFound, "Custom 404"},
} {
var c http.Client
req, err := http.NewRequest("GET", ts.URL+test.path, nil)
if err != nil {
t.Fatal(err)
}
req.Header.Add("Accept", test.accept)
req.Host = host
resp, err := c.Do(req)

if err != nil {
t.Fatalf("error requesting %s: %s", test.path, err)
}

defer resp.Body.Close()
if resp.StatusCode != test.status {
t.Fatalf("got %d, expected %d, from %s", resp.StatusCode, test.status, test.path)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("error reading response from %s: %s", test.path, err)
}

if test.text != "" && string(body) != test.text {
t.Fatalf("unexpected response body from %s: got %q, expected %q", test.path, body, test.text)
}
}
}

func TestIPNSHostnameRedirect(t *testing.T) {
ns := mockNamesys{}
ts, api, ctx := newTestServerAndNode(t, ns)
Expand Down Expand Up @@ -378,7 +442,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
s := string(body)
t.Logf("body: %s\n", string(body))

if !strings.Contains(s, "Index of /foo? #<'/") {
if !strings.Contains(s, "Index of /ipns/example.net/foo? #<'/") {
t.Fatalf("expected a path in directory listing")
}
if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/./..\">") {
Expand Down Expand Up @@ -444,7 +508,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
s = string(body)
t.Logf("body: %s\n", string(body))

if !strings.Contains(s, "Index of /foo? #&lt;&#39;/bar/") {
if !strings.Contains(s, "Index of /ipns/example.net/foo? #&lt;&#39;/bar/") {
t.Fatalf("expected a path in directory listing")
}
if !strings.Contains(s, "<a href=\"/foo%3F%20%23%3C%27/bar/./..\">") {
Expand Down Expand Up @@ -478,7 +542,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
s = string(body)
t.Logf("body: %s\n", string(body))

if !strings.Contains(s, "Index of /good-prefix") {
if !strings.Contains(s, "Index of /ipns/example.net") {
t.Fatalf("expected a path in directory listing")
}
if !strings.Contains(s, "<a href=\"/good-prefix/\">") {
Expand Down
4 changes: 2 additions & 2 deletions gateway/core/corehttp/hostname.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func HostnameOption() ServeOption {
// Not a whitelisted path

// Try DNSLink, if it was not explicitly disabled for the hostname
if !gw.NoDNSLink && isDNSLinkRequest(n.Context(), coreApi, r) {
if !gw.NoDNSLink && isDNSLinkRequest(r.Context(), coreApi, r) {
// rewrite path and handle as DNSLink
r.URL.Path = "/ipns/" + stripPort(r.Host) + r.URL.Path
childMux.ServeHTTP(w, r)
Expand Down Expand Up @@ -176,7 +176,7 @@ func HostnameOption() ServeOption {
// 1. is wildcard DNSLink enabled (Gateway.NoDNSLink=false)?
// 2. does Host header include a fully qualified domain name (FQDN)?
// 3. does DNSLink record exist in DNS?
if !cfg.Gateway.NoDNSLink && isDNSLinkRequest(n.Context(), coreApi, r) {
if !cfg.Gateway.NoDNSLink && isDNSLinkRequest(r.Context(), coreApi, r) {
// rewrite path and handle as DNSLink
r.URL.Path = "/ipns/" + stripPort(r.Host) + r.URL.Path
childMux.ServeHTTP(w, r)
Expand Down
4 changes: 3 additions & 1 deletion gateway/core/corehttp/webui.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package corehttp

// TODO: move to IPNS
const WebUIPath = "/ipfs/bafybeidatpz2hli6fgu3zul5woi27ujesdf5o5a7bu622qj6ugharciwjq"
const WebUIPath = "/ipfs/bafybeigkbbjnltbd4ewfj7elajsbnjwinyk6tiilczkqsibf3o7dcr6nn4" // v2.9.0

// this is a list of all past webUI paths.
var WebUIPaths = []string{
WebUIPath,
"/ipfs/bafybeicp23nbcxtt2k2twyfivcbrc6kr3l5lnaiv3ozvwbemtrb7v52r6i",
"/ipfs/bafybeidatpz2hli6fgu3zul5woi27ujesdf5o5a7bu622qj6ugharciwjq",
"/ipfs/QmfQkD8pBSBCBxWEwFSu4XaDVSWK6bjnNuaWZjMyQbyDub",
"/ipfs/QmXc9raDM1M5G5fpBnVyQ71vR4gbnskwnB9iMEzBuLgvoZ",
"/ipfs/QmenEBWcAk3tN94fSKpKFtUMwty1qNwSYw3DMDFV6cPBXA",
Expand Down

0 comments on commit c5ee923

Please sign in to comment.