mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-21 18:37:45 +08:00
* fix(gateway): do not convert unixfs/raw into dag-* unless explicit * fix(gateway): keep only dag-json|dag-cbor handling * fix: allow requesting dag-json as application/json - adds bunch of additional tests including JSON file on UnixFS - fix: dag-json codec (0x0129) can be returned as plain json - fix: json codec (0x0200) cna be retrurned as plain json * fix: using ?format|Accept with CID w/ codec works * docs(changelog): cbor and json on gateway Co-authored-by: Marcin Rataj <lidel@lidel.org>
258 lines
9.5 KiB
Go
258 lines
9.5 KiB
Go
package corehttp
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"html"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
cid "github.com/ipfs/go-cid"
|
|
ipldlegacy "github.com/ipfs/go-ipld-legacy"
|
|
ipath "github.com/ipfs/interface-go-ipfs-core/path"
|
|
"github.com/ipfs/kubo/assets"
|
|
dih "github.com/ipfs/kubo/assets/dag-index-html"
|
|
"github.com/ipfs/kubo/tracing"
|
|
"github.com/ipld/go-ipld-prime"
|
|
"github.com/ipld/go-ipld-prime/multicodec"
|
|
mc "github.com/multiformats/go-multicodec"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
// codecToContentType maps the supported IPLD codecs to the HTTP Content
|
|
// Type they should have.
|
|
var codecToContentType = map[mc.Code]string{
|
|
mc.Json: "application/json",
|
|
mc.Cbor: "application/cbor",
|
|
mc.DagJson: "application/vnd.ipld.dag-json",
|
|
mc.DagCbor: "application/vnd.ipld.dag-cbor",
|
|
}
|
|
|
|
// contentTypeToRaw maps the HTTP Content Type to the respective codec that
|
|
// allows raw response without any conversion.
|
|
var contentTypeToRaw = map[string][]mc.Code{
|
|
"application/json": {mc.Json, mc.DagJson},
|
|
"application/cbor": {mc.Cbor, mc.DagCbor},
|
|
}
|
|
|
|
// contentTypeToCodec maps the HTTP Content Type to the respective codec. We
|
|
// only add here the codecs that we want to convert-to-from.
|
|
var contentTypeToCodec = map[string]mc.Code{
|
|
"application/vnd.ipld.dag-json": mc.DagJson,
|
|
"application/vnd.ipld.dag-cbor": mc.DagCbor,
|
|
}
|
|
|
|
// contentTypeToExtension maps the HTTP Content Type to the respective file
|
|
// extension, used in Content-Disposition header when downloading the file.
|
|
var contentTypeToExtension = map[string]string{
|
|
"application/json": ".json",
|
|
"application/vnd.ipld.dag-json": ".json",
|
|
"application/cbor": ".cbor",
|
|
"application/vnd.ipld.dag-cbor": ".cbor",
|
|
}
|
|
|
|
func (i *gatewayHandler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, requestedContentType string) {
|
|
ctx, span := tracing.Span(ctx, "Gateway", "ServeCodec", trace.WithAttributes(attribute.String("path", resolvedPath.String()), attribute.String("requestedContentType", requestedContentType)))
|
|
defer span.End()
|
|
|
|
cidCodec := mc.Code(resolvedPath.Cid().Prefix().Codec)
|
|
responseContentType := requestedContentType
|
|
|
|
// If the resolved path still has some remainder, return error for now.
|
|
// TODO: handle this when we have IPLD Patch (https://ipld.io/specs/patch/) via HTTP PUT
|
|
// TODO: (depends on https://github.com/ipfs/kubo/issues/4801 and https://github.com/ipfs/kubo/issues/4782)
|
|
if resolvedPath.Remainder() != "" {
|
|
path := strings.TrimSuffix(resolvedPath.String(), resolvedPath.Remainder())
|
|
err := fmt.Errorf("%q of %q could not be returned: reading IPLD Kinds other than Links (CBOR Tag 42) is not implemented: try reading %q instead", resolvedPath.Remainder(), resolvedPath.String(), path)
|
|
webError(w, "unsupported pathing", err, http.StatusNotImplemented)
|
|
return
|
|
}
|
|
|
|
// If no explicit content type was requested, the response will have one based on the codec from the CID
|
|
if requestedContentType == "" {
|
|
cidContentType, ok := codecToContentType[cidCodec]
|
|
if !ok {
|
|
// Should not happen unless function is called with wrong parameters.
|
|
err := fmt.Errorf("content type not found for codec: %v", cidCodec)
|
|
webError(w, "internal error", err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
responseContentType = cidContentType
|
|
}
|
|
|
|
// Set HTTP headers (for caching etc)
|
|
modtime := addCacheControlHeaders(w, r, contentPath, resolvedPath.Cid())
|
|
name := setCodecContentDisposition(w, r, resolvedPath, responseContentType)
|
|
w.Header().Set("Content-Type", responseContentType)
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
|
|
// No content type is specified by the user (via Accept, or format=). However,
|
|
// we support this format. Let's handle it.
|
|
if requestedContentType == "" {
|
|
isDAG := cidCodec == mc.DagJson || cidCodec == mc.DagCbor
|
|
acceptsHTML := strings.Contains(r.Header.Get("Accept"), "text/html")
|
|
download := r.URL.Query().Get("download") == "true"
|
|
|
|
if isDAG && acceptsHTML && !download {
|
|
i.serveCodecHTML(ctx, w, r, resolvedPath, contentPath)
|
|
} else {
|
|
// This covers CIDs with codec 'json' and 'cbor' as those do not have
|
|
// an explicit requested content type.
|
|
i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// If DAG-JSON or DAG-CBOR was requested using corresponding plain content type
|
|
// return raw block as-is, without conversion
|
|
skipCodecs, ok := contentTypeToRaw[requestedContentType]
|
|
if ok {
|
|
for _, skipCodec := range skipCodecs {
|
|
if skipCodec == cidCodec {
|
|
i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Otherwise, the user has requested a specific content type (a DAG-* variant).
|
|
// Let's first get the codecs that can be used with this content type.
|
|
toCodec, ok := contentTypeToCodec[requestedContentType]
|
|
if !ok {
|
|
// This is never supposed to happen unless function is called with wrong parameters.
|
|
err := fmt.Errorf("unsupported content type: %s", requestedContentType)
|
|
webError(w, err.Error(), err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// This handles DAG-* conversions and validations.
|
|
i.serveCodecConverted(ctx, w, r, resolvedPath, contentPath, toCodec, modtime)
|
|
}
|
|
|
|
func (i *gatewayHandler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path) {
|
|
// 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")
|
|
|
|
// Clear Content-Disposition -- we want HTML to be rendered inline
|
|
w.Header().Del("Content-Disposition")
|
|
|
|
// Generated index requires custom Etag (output may change between Kubo versions)
|
|
dagEtag := getDagIndexEtag(resolvedPath.Cid())
|
|
w.Header().Set("Etag", dagEtag)
|
|
|
|
// Remove Cache-Control for now to match UnixFS dir-index-html responses
|
|
// (we don't want browser to cache HTML forever)
|
|
// TODO: if we ever change behavior for UnixFS dir listings, same changes should be applied here
|
|
w.Header().Del("Cache-Control")
|
|
|
|
cidCodec := mc.Code(resolvedPath.Cid().Prefix().Codec)
|
|
if err := dih.DagIndexTemplate.Execute(w, dih.DagIndexTemplateData{
|
|
Path: contentPath.String(),
|
|
CID: resolvedPath.Cid().String(),
|
|
CodecName: cidCodec.String(),
|
|
CodecHex: fmt.Sprintf("0x%x", uint64(cidCodec)),
|
|
}); err != nil {
|
|
webError(w, "failed to generate HTML listing for this DAG: try fetching raw block with ?format=raw", err, http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// serveCodecRaw returns the raw block without any conversion
|
|
func (i *gatewayHandler) serveCodecRaw(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, name string, modtime time.Time) {
|
|
blockCid := resolvedPath.Cid()
|
|
blockReader, err := i.api.Block().Get(ctx, resolvedPath)
|
|
if err != nil {
|
|
webError(w, "ipfs block get "+blockCid.String(), err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
block, err := io.ReadAll(blockReader)
|
|
if err != nil {
|
|
webError(w, "ipfs block get "+blockCid.String(), err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
content := bytes.NewReader(block)
|
|
|
|
// ServeContent will take care of
|
|
// If-None-Match+Etag, Content-Length and range requests
|
|
_, _, _ = ServeContent(w, r, name, modtime, content)
|
|
}
|
|
|
|
// serveCodecConverted returns payload converted to codec specified in toCodec
|
|
func (i *gatewayHandler) serveCodecConverted(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, toCodec mc.Code, modtime time.Time) {
|
|
obj, err := i.api.Dag().Get(ctx, resolvedPath.Cid())
|
|
if err != nil {
|
|
webError(w, "ipfs dag get "+html.EscapeString(resolvedPath.String()), err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
universal, ok := obj.(ipldlegacy.UniversalNode)
|
|
if !ok {
|
|
err = fmt.Errorf("%T is not a valid IPLD node", obj)
|
|
webError(w, err.Error(), err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
finalNode := universal.(ipld.Node)
|
|
|
|
encoder, err := multicodec.LookupEncoder(uint64(toCodec))
|
|
if err != nil {
|
|
webError(w, err.Error(), err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Ensure IPLD node conforms to the codec specification.
|
|
var buf bytes.Buffer
|
|
err = encoder(finalNode, &buf)
|
|
if err != nil {
|
|
webError(w, err.Error(), err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Sets correct Last-Modified header. This code is borrowed from the standard
|
|
// library (net/http/server.go) as we cannot use serveFile.
|
|
if !(modtime.IsZero() || modtime.Equal(unixEpochTime)) {
|
|
w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
|
|
}
|
|
|
|
_, _ = w.Write(buf.Bytes())
|
|
}
|
|
|
|
func setCodecContentDisposition(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentType string) string {
|
|
var dispType, name string
|
|
|
|
ext, ok := contentTypeToExtension[contentType]
|
|
if !ok {
|
|
// Should never happen.
|
|
ext = ".bin"
|
|
}
|
|
|
|
if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" {
|
|
name = urlFilename
|
|
} else {
|
|
name = resolvedPath.Cid().String() + ext
|
|
}
|
|
|
|
// JSON should be inlined, but ?download=true should still override
|
|
if r.URL.Query().Get("download") == "true" {
|
|
dispType = "attachment"
|
|
} else {
|
|
switch ext {
|
|
case ".json": // codecs that serialize to JSON can be rendered by browsers
|
|
dispType = "inline"
|
|
default: // everything else is assumed binary / opaque bytes
|
|
dispType = "attachment"
|
|
}
|
|
}
|
|
|
|
setContentDispositionHeader(w, name, dispType)
|
|
return name
|
|
}
|
|
|
|
func getDagIndexEtag(dagCid cid.Cid) string {
|
|
return `"DagIndex-` + assets.AssetHash + `_CID-` + dagCid.String() + `"`
|
|
}
|