Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gateway): _redirects file support #8890

Merged
merged 54 commits into from
Sep 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
5ae4e4e
WIP _redirects support
Apr 7, 2022
937e998
Handle forced redirects
Apr 20, 2022
7145efc
Return 404 where we did previously
Apr 20, 2022
7f13047
Remove go.mod replace
Apr 20, 2022
748a6ea
Remove log statements based on CodeQL results
Apr 20, 2022
ab87d10
Add missing test_kill_ipfs_daemon to sharness
Apr 20, 2022
acd5d1a
Deps changes
Apr 21, 2022
8958efe
Comment cleanup
Apr 27, 2022
f79b4b4
Any path resolution errors mean the file doesn't exist
Apr 27, 2022
99720d2
WIP comments
Apr 28, 2022
fc617bf
Check for root path CID before joining with _redirects
Apr 28, 2022
1f86713
DNSLink test
Apr 29, 2022
5d59ce9
Comments
Apr 29, 2022
3cfb84e
Fix placeholder and splat usage for 200 and 404
May 25, 2022
56db72e
go mod tidy
May 25, 2022
0e2a3a8
Move test case to car file
Jun 15, 2022
2e4357b
Update CAR file and test after updating CAR file for spec
Jun 15, 2022
6fcca56
go mod tidy
Jun 16, 2022
08ddc07
Use type for context.WithValue, per docs
Jun 16, 2022
babb6ad
More types
Jun 16, 2022
6830b1b
Remove forced redirect support, to avoid the performance hit
Jun 16, 2022
4d9bddf
Use justincjohnson/go-ipfs-redirects, which I'll switch to the ipfs o…
Jul 7, 2022
d2e6106
Switch to github.com/ipfs-shipyard/go-ipfs-redirects
Aug 11, 2022
3a614ef
Address feedback, correct car fixture
Aug 11, 2022
f66d64b
More feedback
Aug 11, 2022
bb94374
More feedback
Aug 11, 2022
2acbf70
go mod tidy
Aug 11, 2022
f4cf5a6
Fix test
Aug 11, 2022
46faca8
Error early if invalid status
Aug 11, 2022
f9cd964
Confirm CRLF line terminator
Aug 12, 2022
5153b9a
Add tests for invalid _redirects file
Aug 12, 2022
0570b1d
Add test with attempted forced redirect
Aug 12, 2022
f4b9ec2
Simplify getRootPath by using ipath.Path
Aug 12, 2022
3cd0c6d
Consolidate unixfs and non-unixfs handling into a single method.
Aug 12, 2022
4c8ccbd
Remove direct dependency on ucarion/urlpath
Aug 12, 2022
4282785
go mod tidy
Aug 12, 2022
b299f81
Cleanup after rebase
Aug 19, 2022
cf2ae8c
Revert unnecessary downgrade of a dep
Aug 19, 2022
2495b8d
go mod tidy
Sep 15, 2022
825fc1e
function rename, per lidel
Sep 15, 2022
24d248c
Switch from github.com/ipfs-shipyard/go-ipfs-redirects to github.com/…
Sep 15, 2022
9778197
Move status code validation logic to github.com/ipfs/go-ipfs-redirect…
Sep 15, 2022
6334c5a
fix comment, in part to avoid inconsistent go fmt issue with 1.19
Sep 15, 2022
f25ed84
Update comment
Sep 15, 2022
5b6d7e2
Fix test to match new output with status code parsing happening in go…
Sep 16, 2022
38ac69d
Add test for max file size
Sep 16, 2022
2a7b0b6
Refactor as requested in review
Sep 19, 2022
462cd1a
Missing changes from lidel
Sep 21, 2022
8bfa333
fix go mod tidy error in ci
Sep 21, 2022
eb7138d
chore: go-ipfs-redirects-file v0.1.0
lidel Sep 22, 2022
0ae8545
refactor: move handlePathResolution
lidel Sep 22, 2022
a9a0658
Serve custom 410 and 451 pages as well
Sep 23, 2022
e18d6c7
core: go-ipfs-redirects-file v0.1.1
lidel Sep 23, 2022
907dc6d
refactor: move serveLegacy404IfPresent
lidel Sep 23, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion core/coreapi/name.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func (e *ipnsEntry) Value() path.Path {
return e.value
}

type requestContextKey string

// Publish announces new IPNS name and returns the new IPNS entry.
func (api *NameAPI) Publish(ctx context.Context, p path.Path, opts ...caopts.NamePublishOption) (coreiface.IpnsEntry, error) {
ctx, span := tracing.Span(ctx, "CoreAPI.NameAPI", "Publish", trace.WithAttributes(attribute.String("path", p.String())))
Expand Down Expand Up @@ -76,7 +78,7 @@ func (api *NameAPI) Publish(ctx context.Context, p path.Path, opts ...caopts.Nam

if options.TTL != nil {
// nolint: staticcheck // non-backward compatible change
ctx = context.WithValue(ctx, "ipns-publish-ttl", *options.TTL)
ctx = context.WithValue(ctx, requestContextKey("ipns-publish-ttl"), *options.TTL)
justindotpub marked this conversation as resolved.
Show resolved Hide resolved
}

eol := time.Now().Add(options.ValidTime)
Expand Down
136 changes: 47 additions & 89 deletions core/corehttp/gateway_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
gopath "path"
"regexp"
"runtime/debug"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -378,30 +377,18 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
return
}

// Resolve path to the final DAG node for the ETag
resolvedPath, err := i.api.ResolvePath(r.Context(), contentPath)
switch err {
case nil:
case coreiface.ErrOffline:
webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusServiceUnavailable)
return
default:
// if Accept is text/html, see if ipfs-404.html is present
if i.servePretty404IfPresent(w, r, contentPath) {
logger.Debugw("serve pretty 404 if present")
return
}
webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusBadRequest)
return
}

// Detect when explicit Accept header or ?format parameter are present
responseFormat, formatParams, err := customResponseFormat(r)
if err != nil {
webError(w, "error while processing the Accept header", err, http.StatusBadRequest)
return
}
trace.SpanFromContext(r.Context()).SetAttributes(attribute.String("ResponseFormat", responseFormat))

resolvedPath, contentPath, ok := i.handlePathResolution(w, r, responseFormat, contentPath, logger)
if !ok {
return
}
trace.SpanFromContext(r.Context()).SetAttributes(attribute.String("ResolvedPath", resolvedPath.String()))

// Detect when If-None-Match HTTP header allows returning HTTP 304 Not Modified
Expand Down Expand Up @@ -450,36 +437,6 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
}
}

func (i *gatewayHandler) servePretty404IfPresent(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool {
resolved404Path, ctype, err := i.searchUpTreeFor404(r, contentPath)
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.Debugw("using pretty 404 file", "path", contentPath)
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 @@ -920,55 +877,56 @@ func customResponseFormat(r *http.Request) (mediaType string, params map[string]
return "", nil, nil
}

func (i *gatewayHandler) searchUpTreeFor404(r *http.Request, contentPath ipath.Path) (ipath.Resolved, string, error) {
filename404, ctype, err := preferred404Filename(r.Header.Values("Accept"))
if err != nil {
return nil, "", err
// returns unquoted path with all special characters revealed as \u codes
func debugStr(path string) string {
q := fmt.Sprintf("%+q", path)
if len(q) >= 3 {
q = q[1 : len(q)-1]
}
return q
}

pathComponents := strings.Split(contentPath.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
}
// Resolve the provided contentPath including any special handling related to
// the requested responseFormat. Returned ok flag indicates if gateway handler
// should continue processing the request.
func (i *gatewayHandler) handlePathResolution(w http.ResponseWriter, r *http.Request, responseFormat string, contentPath ipath.Path, logger *zap.SugaredLogger) (resolvedPath ipath.Resolved, newContentPath ipath.Path, ok bool) {
// Attempt to resolve the provided path.
resolvedPath, err := i.api.ResolvePath(r.Context(), contentPath)

return nil, "", fmt.Errorf("no pretty 404 in any parent folder")
}
switch err {
case nil:
return resolvedPath, contentPath, true
case coreiface.ErrOffline:
webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusServiceUnavailable)
return nil, nil, false
default:
// The path can't be resolved.
if isUnixfsResponseFormat(responseFormat) {
// If we have origin isolation (subdomain gw, DNSLink website),
// and response type is UnixFS (default for website hosting)
// check for presence of _redirects file and apply rules defined there.
// See: https://github.com/ipfs/specs/pull/290
if hasOriginIsolation(r) {
resolvedPath, newContentPath, ok, hadMatchingRule := i.serveRedirectsIfPresent(w, r, resolvedPath, contentPath, logger)
if hadMatchingRule {
logger.Debugw("applied a rule from _redirects file")
return resolvedPath, newContentPath, ok
}
}

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
// if Accept is text/html, see if ipfs-404.html is present
// This logic isn't documented and will likely be removed at some point.
// Any 404 logic in _redirects above will have already run by this time, so it's really an extra fall back
if i.serveLegacy404IfPresent(w, r, contentPath) {
logger.Debugw("served legacy 404")
return nil, nil, false
}
}
}

return "", "", fmt.Errorf("there is no 404 file for the requested content types")
}

// returns unquoted path with all special characters revealed as \u codes
func debugStr(path string) string {
q := fmt.Sprintf("%+q", path)
if len(q) >= 3 {
q = q[1 : len(q)-1]
// Note: webError will replace http.StatusBadRequest with StatusNotFound if necessary
webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusBadRequest)
return nil, nil, false
}
return q
}

// Detect 'Cache-Control: only-if-cached' in request and return data if it is already in the local datastore.
Expand Down
Loading