feat(gateway): JSON and CBOR response formats (IPIP-328) (#9335)

https://github.com/ipfs/kubo/pull/9335
https://github.com/ipfs/specs/pull/328

Co-authored-by: Marcin Rataj <lidel@lidel.org>
This commit is contained in:
Henrique Dias 2022-12-05 20:22:26 +01:00 committed by GitHub
parent 4d4841f41c
commit fdd19656c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 938 additions and 66 deletions

View File

@ -0,0 +1,3 @@
# dag-index-html
> HTML representation for non-UnixFS DAGs such as DAG-CBOR.

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
# dir-index-html
> Directory listing HTML for `go-ipfs` gateways
> Directory listing HTML for HTTP gateway
![](https://user-images.githubusercontent.com/157609/88379209-ce6f0600-cda2-11ea-9620-20b9237bb441.png)

View File

@ -26,12 +26,12 @@
<div id="page-header">
<div id="page-header-logo" class="ipfs-logo">&nbsp;</div>
<div id="page-header-menu">
<div class="menu-item-wide"><a href="https://ipfs.io" target="_blank" rel="noopener noreferrer">About IPFS</a></div>
<div class="menu-item-wide"><a href="https://ipfs.io#install" target="_blank" rel="noopener noreferrer">Install IPFS</a></div>
<div class="menu-item-narrow"><a href="https://ipfs.io" target="_blank" rel="noopener noreferrer">About</a></div>
<div class="menu-item-narrow"><a href="https://ipfs.io#install" target="_blank" rel="noopener noreferrer">Install</a></div>
<div class="menu-item-wide"><a href="https://ipfs.tech" target="_blank" rel="noopener noreferrer">About IPFS</a></div>
<div class="menu-item-wide"><a href="https://ipfs.tech#install" target="_blank" rel="noopener noreferrer">Install IPFS</a></div>
<div class="menu-item-narrow"><a href="https://ipfs.tech" target="_blank" rel="noopener noreferrer">About</a></div>
<div class="menu-item-narrow"><a href="https://ipfs.tech#install" target="_blank" rel="noopener noreferrer">Install</a></div>
<div>
<a href="https://github.com/ipfs/go-ipfs/issues/new/choose" target="_blank" rel="noopener noreferrer" title="Report a bug">
<a href="https://github.com/ipfs/kubo/issues/new/choose" target="_blank" rel="noopener noreferrer" title="Report a bug">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.4 21"><circle cx="7.5" cy="4.8" r="1"/><circle cx="11.1" cy="4.8" r="1"/><path d="M12.7 8.4c-0.5-1.5-1.9-2.5-3.5-2.5 -1.6 0-3 1-3.5 2.5H12.7z"/><path d="M8.5 9.7H5c-0.5 0.8-0.7 1.7-0.7 2.7 0 2.6 1.8 4.8 4.2 5.2V9.7z"/><path d="M13.4 9.7H9.9v7.9c2.4-0.4 4.2-2.5 4.2-5.2C14.1 11.4 13.9 10.5 13.4 9.7z"/><circle cx="15.7" cy="12.9" r="1"/><circle cx="15.1" cy="15.4" r="1"/><circle cx="15.3" cy="10.4" r="1"/><circle cx="2.7" cy="12.9" r="1"/><circle cx="3.3" cy="15.4" r="1"/><circle cx="3.1" cy="10.4" r="1"/></svg>
</a>
</div>
@ -84,7 +84,7 @@
</td>
<td class="no-linebreak">
{{ if .Hash }}
<a class="ipfs-hash" translate="no" href={{ if $root.DNSLink }}"https://cid.ipfs.io/#{{ .Hash | urlEscape}}" target="_blank" rel="noreferrer noopener"{{ else }}"{{ $root.GatewayURL }}/ipfs/{{ .Hash | urlEscape}}?filename={{ .Name | urlEscape }}"{{ end }}>
<a class="ipfs-hash" translate="no" href={{ if $root.DNSLink }}"https://cid.ipfs.tech/#{{ .Hash | urlEscape}}" target="_blank" rel="noreferrer noopener"{{ else }}"{{ $root.GatewayURL }}/ipfs/{{ .Hash | urlEscape}}?filename={{ .Name | urlEscape }}"{{ end }}>
{{ .ShortHash }}
</a>
{{ end }}

View File

@ -25,12 +25,12 @@
<div id="page-header">
<div id="page-header-logo" class="ipfs-logo">&nbsp;</div>
<div id="page-header-menu">
<div class="menu-item-wide"><a href="https://ipfs.io" target="_blank" rel="noopener noreferrer">About IPFS</a></div>
<div class="menu-item-wide"><a href="https://ipfs.io#install" target="_blank" rel="noopener noreferrer">Install IPFS</a></div>
<div class="menu-item-narrow"><a href="https://ipfs.io" target="_blank" rel="noopener noreferrer">About</a></div>
<div class="menu-item-narrow"><a href="https://ipfs.io#install" target="_blank" rel="noopener noreferrer">Install</a></div>
<div class="menu-item-wide"><a href="https://ipfs.tech" target="_blank" rel="noopener noreferrer">About IPFS</a></div>
<div class="menu-item-wide"><a href="https://ipfs.tech#install" target="_blank" rel="noopener noreferrer">Install IPFS</a></div>
<div class="menu-item-narrow"><a href="https://ipfs.tech" target="_blank" rel="noopener noreferrer">About</a></div>
<div class="menu-item-narrow"><a href="https://ipfs.tech#install" target="_blank" rel="noopener noreferrer">Install</a></div>
<div>
<a href="https://github.com/ipfs/go-ipfs/issues/new/choose" target="_blank" rel="noopener noreferrer" title="Report a bug">
<a href="https://github.com/ipfs/kubo/issues/new/choose" target="_blank" rel="noopener noreferrer" title="Report a bug">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.4 21"><circle cx="7.5" cy="4.8" r="1"/><circle cx="11.1" cy="4.8" r="1"/><path d="M12.7 8.4c-0.5-1.5-1.9-2.5-3.5-2.5 -1.6 0-3 1-3.5 2.5H12.7z"/><path d="M8.5 9.7H5c-0.5 0.8-0.7 1.7-0.7 2.7 0 2.6 1.8 4.8 4.2 5.2V9.7z"/><path d="M13.4 9.7H9.9v7.9c2.4-0.4 4.2-2.5 4.2-5.2C14.1 11.4 13.9 10.5 13.4 9.7z"/><circle cx="15.7" cy="12.9" r="1"/><circle cx="15.1" cy="15.4" r="1"/><circle cx="15.3" cy="10.4" r="1"/><circle cx="2.7" cy="12.9" r="1"/><circle cx="3.3" cy="15.4" r="1"/><circle cx="3.1" cy="10.4" r="1"/></svg>
</a>
</div>
@ -83,7 +83,7 @@
</td>
<td class="no-linebreak">
{{ if .Hash }}
<a class="ipfs-hash" translate="no" href={{ if $root.DNSLink }}"https://cid.ipfs.io/#{{ .Hash | urlEscape}}" target="_blank" rel="noreferrer noopener"{{ else }}"{{ $root.GatewayURL }}/ipfs/{{ .Hash | urlEscape}}?filename={{ .Name | urlEscape }}"{{ end }}>
<a class="ipfs-hash" translate="no" href={{ if $root.DNSLink }}"https://cid.ipfs.tech/#{{ .Hash | urlEscape}}" target="_blank" rel="noreferrer noopener"{{ else }}"{{ $root.GatewayURL }}/ipfs/{{ .Hash | urlEscape}}?filename={{ .Name | urlEscape }}"{{ end }}>
{{ .ShortHash }}
</a>
{{ end }}

View File

@ -26,6 +26,7 @@ import (
coreiface "github.com/ipfs/interface-go-ipfs-core"
ipath "github.com/ipfs/interface-go-ipfs-core/path"
routing "github.com/libp2p/go-libp2p/core/routing"
mc "github.com/multiformats/go-multicodec"
prometheus "github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
@ -417,9 +418,15 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
// Support custom response formats passed via ?format or Accept HTTP header
switch responseFormat {
case "": // The implicit response format is UnixFS
logger.Debugw("serving unixfs", "path", contentPath)
i.serveUnixFS(r.Context(), w, r, resolvedPath, contentPath, begin, logger)
case "":
switch resolvedPath.Cid().Prefix().Codec {
case uint64(mc.Json), uint64(mc.DagJson), uint64(mc.Cbor), uint64(mc.DagCbor):
logger.Debugw("serving codec", "path", contentPath)
i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat)
default:
logger.Debugw("serving unixfs", "path", contentPath)
i.serveUnixFS(r.Context(), w, r, resolvedPath, contentPath, begin, logger)
}
return
case "application/vnd.ipld.raw":
logger.Debugw("serving raw block", "path", contentPath)
@ -434,6 +441,11 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
logger.Debugw("serving tar file", "path", contentPath)
i.serveTAR(r.Context(), w, r, resolvedPath, contentPath, begin, logger)
return
case "application/json", "application/vnd.ipld.dag-json",
"application/cbor", "application/vnd.ipld.dag-cbor":
logger.Debugw("serving codec", "path", contentPath)
i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat)
return
default: // catch-all for unsuported application/vnd.*
err := fmt.Errorf("unsupported format %q", responseFormat)
webError(w, "failed respond with requested content type", err, http.StatusBadRequest)
@ -866,6 +878,14 @@ func customResponseFormat(r *http.Request) (mediaType string, params map[string]
return "application/vnd.ipld.car", nil, nil
case "tar":
return "application/x-tar", nil, nil
case "dag-json":
return "application/vnd.ipld.dag-json", nil, nil
case "json":
return "application/json", nil, nil
case "dag-cbor":
return "application/vnd.ipld.dag-cbor", nil, nil
case "cbor":
return "application/cbor", nil, nil
}
}
// Browsers and other user agents will send Accept header with generic types like:
@ -874,7 +894,9 @@ func customResponseFormat(r *http.Request) (mediaType string, params map[string]
for _, accept := range r.Header.Values("Accept") {
// respond to the very first ipld content type
if strings.HasPrefix(accept, "application/vnd.ipld") ||
strings.HasPrefix(accept, "application/x-tar") {
strings.HasPrefix(accept, "application/x-tar") ||
strings.HasPrefix(accept, "application/json") ||
strings.HasPrefix(accept, "application/cbor") {
mediatype, params, err := mime.ParseMediaType(accept)
if err != nil {
return "", nil, err

View File

@ -0,0 +1,258 @@
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[uint64]string{
uint64(mc.Json): "application/json",
uint64(mc.Cbor): "application/cbor",
uint64(mc.DagJson): "application/vnd.ipld.dag-json",
uint64(mc.DagCbor): "application/vnd.ipld.dag-cbor",
}
// contentTypeToCodecs maps the HTTP Content Type to the respective
// possible codecs. If the original data is in one of those codecs,
// we stream the raw bytes. Otherwise, we encode in the last codec
// of the list.
var contentTypeToCodecs = map[string][]uint64{
"application/json": {uint64(mc.Json), uint64(mc.DagJson)},
"application/vnd.ipld.dag-json": {uint64(mc.DagJson)},
"application/cbor": {uint64(mc.Cbor), uint64(mc.DagCbor)},
"application/vnd.ipld.dag-cbor": {uint64(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 := 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 == uint64(mc.DagJson) || cidCodec == uint64(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 {
i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime)
}
return
}
// Otherwise, the user has requested a specific content type. Let's first get
// the codecs that can be used with this content type.
codecs, ok := contentTypeToCodecs[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
}
// If we need to convert, use the last codec (strict dag- variant)
toCodec := codecs[len(codecs)-1]
// If the requested content type has "dag-", ALWAYS go through the encoding
// process in order to validate the content.
if strings.Contains(requestedContentType, "dag-") {
i.serveCodecConverted(ctx, w, r, resolvedPath, contentPath, toCodec, modtime)
return
}
// Otherwise, check if the data is encoded with the requested content type.
// If so, we can directly stream the raw data. serveRawBlock cannot be directly
// used here as it sets different headers.
for _, codec := range codecs {
if resolvedPath.Cid().Prefix().Codec == codec {
i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime)
return
}
}
// Finally, if nothing of the above is true, we have to actually convert the codec.
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)
}
}
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)
}
func (i *gatewayHandler) serveCodecConverted(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, toCodec uint64, 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(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() + `"`
}

View File

@ -498,9 +498,9 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
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, "<a class=\"ipfs-hash\" translate=\"no\" href=\"https://cid.ipfs.io/#") {
if !strings.Contains(s, "<a class=\"ipfs-hash\" translate=\"no\" href=\"https://cid.ipfs.tech/#") {
// https://github.com/ipfs/dir-index-html/issues/42
t.Fatalf("expected links to cid.ipfs.io in CID column when on DNSLink website")
t.Fatalf("expected links to cid.ipfs.tech in CID column when on DNSLink website")
}
if !strings.Contains(s, k2.Cid().String()) {
t.Fatalf("expected hash in directory listing")
@ -535,9 +535,9 @@ func TestIPNSHostnameBacklinks(t *testing.T) {
if !strings.Contains(s, "<a href=\"/file.txt\">") {
t.Fatalf("expected file in directory listing")
}
if !strings.Contains(s, "<a class=\"ipfs-hash\" translate=\"no\" href=\"https://cid.ipfs.io/#") {
if !strings.Contains(s, "<a class=\"ipfs-hash\" translate=\"no\" href=\"https://cid.ipfs.tech/#") {
// https://github.com/ipfs/dir-index-html/issues/42
t.Fatalf("expected links to cid.ipfs.io in CID column when on DNSLink website")
t.Fatalf("expected links to cid.ipfs.tech in CID column when on DNSLink website")
}
if !strings.Contains(s, k.Cid().String()) {
t.Fatalf("expected hash in directory listing")

73
docs/changelogs/v0.18.md Normal file
View File

@ -0,0 +1,73 @@
# Kubo changelog v0.18
## v0.18.0
### Overview
Below is an outline of all that is in this release, so you get a sense of all that's included.
- [Kubo changelog v0.18](#kubo-changelog-v018)
- [v0.18.0](#v0180)
- [Overview](#overview)
- [🔦 Highlights](#-highlights)
- [(DAG-)JSON and (DAG-)CBOR Response Formats on Gateways](#dag-json-and-dag-cbor-response-formats-on-gateways)
- [Changelog](#changelog)
- [Contributors](#contributors)
### 🔦 Highlights
#### (DAG-)JSON and (DAG-)CBOR Response Formats on Gateways
Implemented [IPIP-328](https://github.com/ipfs/specs/pull/328) which adds support
to DAG-JSON and DAG-CBOR, as well as their non-DAG variants, to the gateway. Now,
CIDs that encode JSON, CBOR, DAG-JSON and DAG-CBOR objects can be retrieved, and
traversed through IPLD Links.
HTTP clients can request JSON, CBOR, DAG-JSON, and DAG-CBOR responses by either
passing the query parameter `?format` or setting the `Accept` HTTP header to the
following values:
- JSON: `?format=json`, or `Accept: application/json`
- CBOR: `?format=cbor`, or `Accept: application/cbor`
- DAG-JSON: `?format=dag-json`, or `Accept: application/vnd.ipld.dag-json`
- DAG-JSON: `?format=dag-cbor`, or `Accept: application/vnd.ipld.dag-cbor`
```console
$ export DIR_CID=bafybeigccimv3zqm5g4jt363faybagywkvqbrismoquogimy7kvz2sj7sq
$ curl -H "Accept: application/vnd.ipld.dag-json" "http://127.0.0.1:8080/ipfs/$DIR_CID" | jq
$ curl "http://127.0.0.1:8080/ipfs/$DIR_CID?format=dag-json" | jq
{
"Data": {
"/": {
"bytes": "CAE"
}
},
"Links": [
{
"Hash": {
"/": "Qmc3zqKcwzbbvw3MQm3hXdg8BQoFjGdZiGdAfXAyAGGdLi"
},
"Name": "1 - Barrel - Part 1 - alt.txt",
"Tsize": 21
},
{
"Hash": {
"/": "QmdMxMx29KVYhHnaCc1icWYxQqXwUNCae6t1wS2NqruiHd"
},
"Name": "1 - Barrel - Part 1 - transcript.txt",
"Tsize": 195
},
{
"Hash": {
"/": "QmawceGscqN4o8Y8Fv26UUmB454kn2bnkXV5tEQYc4jBd6"
},
"Name": "1 - Barrel - Part 1.png",
"Tsize": 24862
}
]
}
```
### Changelog
### Contributors

View File

@ -159,8 +159,8 @@ test_expect_success "dnslink gw: name column should be a link to content root mo
# DNSLink websites don't have public gateway mounted by default
# See: https://github.com/ipfs/dir-index-html/issues/42
test_expect_success "dnslink gw: hash column should be a CID link to cid.ipfs.io" '
test_should_contain "<a class=\"ipfs-hash\" translate=\"no\" href=\"https://cid.ipfs.io/#$FILE_CID\" target=\"_blank\" rel=\"noreferrer noopener\">" list_response
test_expect_success "dnslink gw: hash column should be a CID link to cid.ipfs.tech" '
test_should_contain "<a class=\"ipfs-hash\" translate=\"no\" href=\"https://cid.ipfs.tech/#$FILE_CID\" target=\"_blank\" rel=\"noreferrer noopener\">" list_response
'
## ============================================================================

View File

@ -16,10 +16,14 @@ test_launch_ipfs_daemon_without_network
# - have implicit index.html for a good measure
# /ipns/root1/root2/root3/ (/ipns/root1/root2/root3/index.html)
# Note: we cover important edge case here:
# Note: we cover important UnixFS-focused edge case here:
#
# ROOT3_CID - dir listing (dir-index-html response)
# ROOT4_CID - index.html returned as a root response (dir/), instead of generated dir-index-html
# FILE_CID - index.html returned directly, as a file
#
# Caching of things like raw blocks, CARs, dag-json and dag-cbor
# is tested in their respective suites.
test_expect_success "Add the test directory" '
mkdir -p root2/root3/root4 &&
@ -41,30 +45,36 @@ test_expect_success "Prepare IPNS unixfs content path for testing" '
'
# GET /ipfs/
# unixfs
test_expect_success "GET for /ipfs/ unixfs dir listing succeeds" '
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT1_CID/root2/root3/" >/dev/null 2>curl_ipfs_dir_listing_output &&
cat curl_ipfs_dir_listing_output
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT1_CID/root2/root3/" >/dev/null 2>curl_ipfs_dir_listing_output
'
test_expect_success "GET for /ipfs/ unixfs dir with index.html succeeds" '
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT1_CID/root2/root3/root4/" >/dev/null 2>curl_ipfs_dir_index.html_output &&
cat curl_ipfs_dir_index.html_output
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT1_CID/root2/root3/root4/" >/dev/null 2>curl_ipfs_dir_index.html_output
'
test_expect_success "GET for /ipfs/ unixfs file succeeds" '
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT1_CID/root2/root3/root4/index.html" >/dev/null 2>curl_ipfs_file_output &&
cat curl_ipfs_file_output
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT1_CID/root2/root3/root4/index.html" >/dev/null 2>curl_ipfs_file_output
'
# unixfs dir as dag-json
test_expect_success "GET for /ipfs/ unixfs dir as DAG-JSON succeeds" '
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT1_CID/root2/root3/root4/?format=dag-json" >/dev/null 2>curl_ipfs_dir_dag-json_output &&
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT1_CID/root2/root3/root4/?format=json" >/dev/null 2>curl_ipfs_dir_json_output
'
# GET /ipns/
# unixfs
test_expect_success "GET for /ipns/ unixfs dir listing succeeds" '
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipns/$TEST_IPNS_ID/root2/root3/" >/dev/null 2>curl_ipns_dir_listing_output &&
cat curl_ipns_dir_listing_output
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipns/$TEST_IPNS_ID/root2/root3/" >/dev/null 2>curl_ipns_dir_listing_output
'
test_expect_success "GET for /ipns/ unixfs dir with index.html succeeds" '
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipns/$TEST_IPNS_ID/root2/root3/root4/" >/dev/null 2>curl_ipns_dir_index.html_output &&
cat curl_ipns_dir_index.html_output
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipns/$TEST_IPNS_ID/root2/root3/root4/" >/dev/null 2>curl_ipns_dir_index.html_output
'
test_expect_success "GET for /ipns/ unixfs file succeeds" '
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipns/$TEST_IPNS_ID/root2/root3/root4/index.html" >/dev/null 2>curl_ipns_file_output &&
cat curl_ipns_file_output
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipns/$TEST_IPNS_ID/root2/root3/root4/index.html" >/dev/null 2>curl_ipns_file_output
'
# unixfs dir as dag-json
test_expect_success "GET for /ipns/ unixfs dir as DAG-JSON succeeds" '
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipns/$TEST_IPNS_ID/root2/root3/root4/?format=dag-json" >/dev/null 2>curl_ipns_dir_dag-json_output &&
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipns/$TEST_IPNS_ID/root2/root3/root4/?format=json" >/dev/null 2>curl_ipns_dir_json_output
'
# Cache-Control
@ -82,19 +92,34 @@ test_expect_success "Prepare IPNS unixfs content path for testing" '
test_expect_success "GET /ipfs/ unixfs dir with index.html has expected Cache-Control" '
test_should_contain "< Cache-Control: public, max-age=29030400, immutable" curl_ipfs_dir_index.html_output
'
# Cache-Control: immutable /ipfs/ unixfs dir as dag-json
test_expect_success "GET /ipfs/ dag-json has expected Cache-Control" '
test_should_contain "< Cache-Control: public, max-age=29030400, immutable" curl_ipfs_dir_dag-json_output
'
# Cache-Control: immutable /ipfs/ unixfs dir as json
test_expect_success "GET /ipfs/ unixfs dir as json has expected Cache-Control" '
test_should_contain "< Cache-Control: public, max-age=29030400, immutable" curl_ipfs_dir_json_output
'
# Cache-Control: mutable /ipns/ file
test_expect_success "GET /ipns/ unixfs file has no Cache-Control" '
test_should_not_contain "< Cache-Control" curl_ipns_file_output
'
# Cache-Control: generated /ipns/dir/ (listing)
# Cache-Control: mutable /ipns/dir/ (generated listing)
test_expect_success "GET /ipns/ unixfs dir listing has no Cache-Control" '
test_should_not_contain "< Cache-Control" curl_ipns_dir_listing_output
'
# Cache-Control: immutable /ipns/dir/ (index.html)
# Cache-Control: mutable /ipns/dir/ (index.html)
test_expect_success "GET /ipns/ unixfs dir with index.html has no Cache-Control" '
test_should_not_contain "< Cache-Control" curl_ipns_dir_index.html_output
'
# Cache-Control: mutable /ipns/dir/ as dag-json
test_expect_success "GET /ipns/ unixfs dir as dag-json has no Cache-Control" '
test_should_not_contain "< Cache-Control" curl_ipns_dir_dag-json_output
'
# Cache-Control: mutable /ipns/dir/ as json
test_expect_success "GET /ipns/ unixfs dir as json has no Cache-Control" '
test_should_not_contain "< Cache-Control" curl_ipns_dir_json_output
'
# Cache-Control: only-if-cached
test_expect_success "HEAD for /ipfs/ with only-if-cached succeeds when in local datastore" '
@ -122,78 +147,80 @@ test_expect_success "Prepare IPNS unixfs content path for testing" '
## dir generated listing
test_expect_success "GET /ipfs/ dir listing response has original content path in X-Ipfs-Path" '
grep "< X-Ipfs-Path: /ipfs/$ROOT1_CID/root2/root3" curl_ipfs_dir_listing_output
test_should_contain "< X-Ipfs-Path: /ipfs/$ROOT1_CID/root2/root3" curl_ipfs_dir_listing_output
'
test_expect_success "GET /ipns/ dir listing response has original content path in X-Ipfs-Path" '
grep "< X-Ipfs-Path: /ipns/$TEST_IPNS_ID/root2/root3" curl_ipns_dir_listing_output
test_should_contain "< X-Ipfs-Path: /ipns/$TEST_IPNS_ID/root2/root3" curl_ipns_dir_listing_output
'
## dir static index.html
test_expect_success "GET /ipfs/ dir index.html response has original content path in X-Ipfs-Path" '
grep "< X-Ipfs-Path: /ipfs/$ROOT1_CID/root2/root3/root4/" curl_ipfs_dir_index.html_output
test_should_contain "< X-Ipfs-Path: /ipfs/$ROOT1_CID/root2/root3/root4/" curl_ipfs_dir_index.html_output
'
test_expect_success "GET /ipns/ dir index.html response has original content path in X-Ipfs-Path" '
grep "< X-Ipfs-Path: /ipns/$TEST_IPNS_ID/root2/root3/root4/" curl_ipns_dir_index.html_output
test_should_contain "< X-Ipfs-Path: /ipns/$TEST_IPNS_ID/root2/root3/root4/" curl_ipns_dir_index.html_output
'
# file
test_expect_success "GET /ipfs/ file response has original content path in X-Ipfs-Path" '
grep "< X-Ipfs-Path: /ipfs/$ROOT1_CID/root2/root3/root4/index.html" curl_ipfs_file_output
test_should_contain "< X-Ipfs-Path: /ipfs/$ROOT1_CID/root2/root3/root4/index.html" curl_ipfs_file_output
'
test_expect_success "GET /ipns/ file response has original content path in X-Ipfs-Path" '
grep "< X-Ipfs-Path: /ipns/$TEST_IPNS_ID/root2/root3/root4/index.html" curl_ipns_file_output
test_should_contain "< X-Ipfs-Path: /ipns/$TEST_IPNS_ID/root2/root3/root4/index.html" curl_ipns_file_output
'
# X-Ipfs-Roots
## dir generated listing
test_expect_success "GET /ipfs/ dir listing response has logical CID roots in X-Ipfs-Roots" '
grep "< X-Ipfs-Roots: ${ROOT1_CID},${ROOT2_CID},${ROOT3_CID}" curl_ipfs_dir_listing_output
test_should_contain "< X-Ipfs-Roots: ${ROOT1_CID},${ROOT2_CID},${ROOT3_CID}" curl_ipfs_dir_listing_output
'
test_expect_success "GET /ipns/ dir listing response has logical CID roots in X-Ipfs-Roots" '
grep "< X-Ipfs-Roots: ${ROOT1_CID},${ROOT2_CID},${ROOT3_CID}" curl_ipns_dir_listing_output
test_should_contain "< X-Ipfs-Roots: ${ROOT1_CID},${ROOT2_CID},${ROOT3_CID}" curl_ipns_dir_listing_output
'
## dir static index.html
test_expect_success "GET /ipfs/ dir index.html response has logical CID roots in X-Ipfs-Roots" '
grep "< X-Ipfs-Roots: ${ROOT1_CID},${ROOT2_CID},${ROOT3_CID},${ROOT4_CID}" curl_ipfs_dir_index.html_output
test_should_contain "< X-Ipfs-Roots: ${ROOT1_CID},${ROOT2_CID},${ROOT3_CID},${ROOT4_CID}" curl_ipfs_dir_index.html_output
'
test_expect_success "GET /ipns/ dir index.html response has logical CID roots in X-Ipfs-Roots" '
grep "< X-Ipfs-Roots: ${ROOT1_CID},${ROOT2_CID},${ROOT3_CID},${ROOT4_CID}" curl_ipns_dir_index.html_output
test_should_contain "< X-Ipfs-Roots: ${ROOT1_CID},${ROOT2_CID},${ROOT3_CID},${ROOT4_CID}" curl_ipns_dir_index.html_output
'
## file
test_expect_success "GET /ipfs/ file response has logical CID roots in X-Ipfs-Roots" '
grep "< X-Ipfs-Roots: ${ROOT1_CID},${ROOT2_CID},${ROOT3_CID},${ROOT4_CID},${FILE_CID}" curl_ipfs_file_output
test_should_contain "< X-Ipfs-Roots: ${ROOT1_CID},${ROOT2_CID},${ROOT3_CID},${ROOT4_CID},${FILE_CID}" curl_ipfs_file_output
'
test_expect_success "GET /ipns/ file response has logical CID roots in X-Ipfs-Roots" '
grep "< X-Ipfs-Roots: ${ROOT1_CID},${ROOT2_CID},${ROOT3_CID},${ROOT4_CID},${FILE_CID}" curl_ipns_file_output
test_should_contain "< X-Ipfs-Roots: ${ROOT1_CID},${ROOT2_CID},${ROOT3_CID},${ROOT4_CID},${FILE_CID}" curl_ipns_file_output
'
# Etag
## dir generated listing
test_expect_success "GET /ipfs/ dir response has special Etag for generated dir listing" '
test_should_contain "< Etag: \"DirIndex" curl_ipfs_dir_listing_output &&
grep -E "< Etag: \"DirIndex-.+_CID-${ROOT3_CID}\"" curl_ipfs_dir_listing_output
'
test_expect_success "GET /ipns/ dir response has special Etag for generated dir listing" '
test_should_contain "< Etag: \"DirIndex" curl_ipfs_dir_listing_output &&
grep -E "< Etag: \"DirIndex-.+_CID-${ROOT3_CID}\"" curl_ipns_dir_listing_output
'
## dir static index.html should use CID of the index.html file for improved HTTP caching
test_expect_success "GET /ipfs/ dir index.html response has dir CID as Etag" '
grep "< Etag: \"${ROOT4_CID}\"" curl_ipfs_dir_index.html_output
test_should_contain "< Etag: \"${ROOT4_CID}\"" curl_ipfs_dir_index.html_output
'
test_expect_success "GET /ipns/ dir index.html response has dir CID as Etag" '
grep "< Etag: \"${ROOT4_CID}\"" curl_ipns_dir_index.html_output
test_should_contain "< Etag: \"${ROOT4_CID}\"" curl_ipns_dir_index.html_output
'
## file
test_expect_success "GET /ipfs/ response has CID as Etag for a file" '
grep "< Etag: \"${FILE_CID}\"" curl_ipfs_file_output
test_should_contain "< Etag: \"${FILE_CID}\"" curl_ipfs_file_output
'
test_expect_success "GET /ipns/ response has CID as Etag for a file" '
grep "< Etag: \"${FILE_CID}\"" curl_ipns_file_output
test_should_contain "< Etag: \"${FILE_CID}\"" curl_ipns_file_output
'
# If-None-Match (return 304 Not Modified when client sends matching Etag they already have)

View File

@ -32,43 +32,41 @@ test_expect_success "Create text fixtures" '
test_expect_success "GET response for application/vnd.ipld.raw has expected Content-Type" '
curl -svX GET -H "Accept: application/vnd.ipld.raw" "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT_DIR_CID/dir/ascii.txt" >/dev/null 2>curl_output &&
cat curl_output &&
grep "< Content-Type: application/vnd.ipld.raw" curl_output
test_should_contain "< Content-Type: application/vnd.ipld.raw" curl_output
'
test_expect_success "GET response for application/vnd.ipld.raw includes Content-Length" '
BYTES=$(ipfs block get $FILE_CID | wc --bytes)
grep "< Content-Length: $BYTES" curl_output
test_should_contain "< Content-Length: $BYTES" curl_output
'
test_expect_success "GET response for application/vnd.ipld.raw includes Content-Disposition" '
grep "< Content-Disposition: attachment\; filename=\"${FILE_CID}.bin\"" curl_output
test_should_contain "< Content-Disposition: attachment\; filename=\"${FILE_CID}.bin\"" curl_output
'
test_expect_success "GET response for application/vnd.ipld.raw includes nosniff hint" '
grep "< X-Content-Type-Options: nosniff" curl_output
test_should_contain "< X-Content-Type-Options: nosniff" curl_output
'
test_expect_success "GET for application/vnd.ipld.raw with query filename includes Content-Disposition with custom filename" '
curl -svX GET -H "Accept: application/vnd.ipld.raw" "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT_DIR_CID/dir/ascii.txt?filename=foobar.bin" >/dev/null 2>curl_output_filename &&
cat curl_output_filename &&
grep "< Content-Disposition: attachment\; filename=\"foobar.bin\"" curl_output_filename
test_should_contain "< Content-Disposition: attachment\; filename=\"foobar.bin\"" curl_output_filename
'
# Cache control HTTP headers
# (basic checks, detailed behavior is tested in t0116-gateway-cache.sh)
test_expect_success "GET response for application/vnd.ipld.raw includes Etag" '
grep "< Etag: \"${FILE_CID}.raw\"" curl_output
test_should_contain "< Etag: \"${FILE_CID}.raw\"" curl_output
'
test_expect_success "GET response for application/vnd.ipld.raw includes X-Ipfs-Path and X-Ipfs-Roots" '
grep "< X-Ipfs-Path" curl_output &&
grep "< X-Ipfs-Roots" curl_output
test_should_contain "< X-Ipfs-Path" curl_output &&
test_should_contain "< X-Ipfs-Roots" curl_output
'
test_expect_success "GET response for application/vnd.ipld.raw includes Cache-Control" '
grep "< Cache-Control: public, max-age=29030400, immutable" curl_output
test_should_contain "< Cache-Control: public, max-age=29030400, immutable" curl_output
'
test_kill_ipfs_daemon

View File

@ -0,0 +1,387 @@
#!/usr/bin/env bash
test_description="Test HTTP Gateway DAG-JSON (application/vnd.ipld.dag-json) and DAG-CBOR (application/vnd.ipld.dag-cbor) Support"
. lib/test-lib.sh
test_init_ipfs
test_launch_ipfs_daemon_without_network
test_expect_success "Add the test directory" '
mkdir -p rootDir/ipfs &&
mkdir -p rootDir/ipns &&
mkdir -p rootDir/api &&
mkdir -p rootDir/ą/ę &&
echo "I am a txt file on path with utf8" > rootDir/ą/ę/file-źł.txt &&
echo "I am a txt file in confusing /api dir" > rootDir/api/file.txt &&
echo "I am a txt file in confusing /ipfs dir" > rootDir/ipfs/file.txt &&
echo "I am a txt file in confusing /ipns dir" > rootDir/ipns/file.txt &&
DIR_CID=$(ipfs add -Qr --cid-version 1 rootDir) &&
FILE_CID=$(ipfs files stat --enc=json /ipfs/$DIR_CID/ą/ę/file-źł.txt | jq -r .Hash) &&
FILE_SIZE=$(ipfs files stat --enc=json /ipfs/$DIR_CID/ą/ę/file-źł.txt | jq -r .Size)
echo "$FILE_CID / $FILE_SIZE"
'
## Reading UnixFS (data encoded with dag-pb codec) as DAG-CBOR and DAG-JSON
test_dag_pb_headers () {
name=$1
format=$2
disposition=$3
test_expect_success "GET UnixFS as $name with format=dag-$format has expected Content-Type" '
curl -sD - "http://127.0.0.1:$GWAY_PORT/ipfs/$FILE_CID?format=dag-$format" > curl_output 2>&1 &&
test_should_contain "Content-Type: application/vnd.ipld.dag-$format" curl_output &&
test_should_contain "Content-Disposition: ${disposition}\; filename=\"${FILE_CID}.${format}\"" curl_output &&
test_should_not_contain "Content-Type: application/$format" curl_output
'
test_expect_success "GET UnixFS as $name with 'Accept: application/vnd.ipld.dag-$format' has expected Content-Type" '
curl -sD - -H "Accept: application/vnd.ipld.dag-$format" "http://127.0.0.1:$GWAY_PORT/ipfs/$FILE_CID" > curl_output 2>&1 &&
test_should_contain "Content-Disposition: ${disposition}\; filename=\"${FILE_CID}.${format}\"" curl_output &&
test_should_contain "Content-Type: application/vnd.ipld.dag-$format" curl_output &&
test_should_not_contain "Content-Type: application/$format" curl_output
'
test_expect_success "GET UnixFS as $name with format=$format has expected Content-Type" '
curl -sD - "http://127.0.0.1:$GWAY_PORT/ipfs/$FILE_CID?format=$format" > curl_output 2>&1 &&
test_should_contain "Content-Disposition: ${disposition}\; filename=\"${FILE_CID}.${format}\"" curl_output &&
test_should_contain "Content-Type: application/$format" curl_output &&
test_should_not_contain "Content-Type: application/vnd.ipld.dag-$format" curl_output
'
test_expect_success "GET UnixFS as $name with 'Accept: application/$format' has expected Content-Type" '
curl -sD - -H "Accept: application/$format" "http://127.0.0.1:$GWAY_PORT/ipfs/$FILE_CID" > curl_output 2>&1 &&
test_should_contain "Content-Disposition: ${disposition}\; filename=\"${FILE_CID}.${format}\"" curl_output &&
test_should_contain "Content-Type: application/$format" curl_output &&
test_should_not_contain "Content-Type: application/vnd.ipld.dag-$format" curl_output
'
}
test_dag_pb_headers "DAG-JSON" "json" "inline"
test_dag_pb_headers "DAG-CBOR" "cbor" "attachment"
test_dag_pb () {
name=$1
format=$2
test_expect_success "GET UnixFS as $name has expected output for file" '
curl -s "http://127.0.0.1:$GWAY_PORT/ipfs/$FILE_CID?format=dag-$format" > curl_output 2>&1 &&
ipfs dag get --output-codec dag-$format $FILE_CID > ipfs_dag_get_output 2>&1 &&
test_cmp ipfs_dag_get_output curl_output
'
test_expect_success "GET UnixFS as $name has expected output for directory" '
curl -s "http://127.0.0.1:$GWAY_PORT/ipfs/$DIR_CID?format=dag-$format" > curl_output 2>&1 &&
ipfs dag get --output-codec dag-$format $DIR_CID > ipfs_dag_get_output 2>&1 &&
test_cmp ipfs_dag_get_output curl_output
'
test_expect_success "GET UnixFS as $name with format=dag-$format and format=$format produce same output" '
curl -s "http://127.0.0.1:$GWAY_PORT/ipfs/$DIR_CID?format=dag-$format" > curl_output_1 2>&1 &&
curl -s "http://127.0.0.1:$GWAY_PORT/ipfs/$DIR_CID?format=$format" > curl_output_2 2>&1 &&
test_cmp curl_output_1 curl_output_2
'
}
test_dag_pb "DAG-JSON" "json"
test_dag_pb "DAG-CBOR" "cbor"
## Content-Type response based on Accept header and ?format= parameter
test_cmp_dag_get () {
name=$1
format=$2
disposition=$3
test_expect_success "GET $name without Accept or format= has expected Content-Type" '
CID=$(echo "{ \"test\": \"json\" }" | ipfs dag put --input-codec json --store-codec $format) &&
curl -sD - "http://127.0.0.1:$GWAY_PORT/ipfs/$CID" > curl_output 2>&1 &&
test_should_contain "Content-Disposition: ${disposition}\; filename=\"${CID}.${format}\"" curl_output &&
test_should_contain "Content-Type: application/$format" curl_output
'
test_expect_success "GET $name without Accept or format= produces correct output" '
CID=$(echo "{ \"test\": \"json\" }" | ipfs dag put --input-codec json --store-codec $format) &&
curl -s "http://127.0.0.1:$GWAY_PORT/ipfs/$CID" > curl_output 2>&1 &&
ipfs dag get --output-codec $format $CID > ipfs_dag_get_output 2>&1 &&
test_cmp ipfs_dag_get_output curl_output
'
test_expect_success "GET $name with format=$format produces expected Content-Type" '
CID=$(echo "{ \"test\": \"json\" }" | ipfs dag put --input-codec json --store-codec $format) &&
curl -sD- "http://127.0.0.1:$GWAY_PORT/ipfs/$CID?format=$format" > curl_output 2>&1 &&
test_should_contain "Content-Disposition: ${disposition}\; filename=\"${CID}.${format}\"" curl_output &&
test_should_contain "Content-Type: application/$format" curl_output
'
test_expect_success "GET $name with format=$format produces correct output" '
CID=$(echo "{ \"test\": \"json\" }" | ipfs dag put --input-codec json --store-codec $format) &&
curl -s "http://127.0.0.1:$GWAY_PORT/ipfs/$CID?format=$format" > curl_output 2>&1 &&
ipfs dag get --output-codec $format $CID > ipfs_dag_get_output 2>&1 &&
test_cmp ipfs_dag_get_output curl_output
'
test_expect_success "GET $name with format=dag-$format produces expected Content-Type" '
CID=$(echo "{ \"test\": \"json\" }" | ipfs dag put --input-codec json --store-codec $format) &&
curl -sD- "http://127.0.0.1:$GWAY_PORT/ipfs/$CID?format=dag-$format" > curl_output 2>&1 &&
test_should_contain "Content-Disposition: ${disposition}\; filename=\"${CID}.${format}\"" curl_output &&
test_should_contain "Content-Type: application/vnd.ipld.dag-$format" curl_output
'
test_expect_success "GET $name with format=dag-$format produces correct output" '
CID=$(echo "{ \"test\": \"json\" }" | ipfs dag put --input-codec json --store-codec $format) &&
curl -s "http://127.0.0.1:$GWAY_PORT/ipfs/$CID?format=dag-$format" > curl_output 2>&1 &&
ipfs dag get --output-codec dag-$format $CID > ipfs_dag_get_output 2>&1 &&
test_cmp ipfs_dag_get_output curl_output
'
}
test_cmp_dag_get "JSON" "json" "inline"
test_cmp_dag_get "CBOR" "cbor" "attachment"
## Lossless conversion between JSON and CBOR
test_expect_success "GET JSON as CBOR produces DAG-CBOR output" '
CID=$(echo "{ \"test\": \"json\" }" | ipfs dag put --input-codec json --store-codec json) &&
curl -s "http://127.0.0.1:$GWAY_PORT/ipfs/$CID?format=cbor" > curl_output 2>&1 &&
ipfs dag get --output-codec dag-cbor $CID > ipfs_dag_get_output 2>&1 &&
test_cmp ipfs_dag_get_output curl_output
'
test_expect_success "GET CBOR as JSON produces DAG-JSON output" '
CID=$(echo "{ \"test\": \"json\" }" | ipfs dag put --input-codec json --store-codec cbor) &&
curl -s "http://127.0.0.1:$GWAY_PORT/ipfs/$CID?format=json" > curl_output 2>&1 &&
ipfs dag get --output-codec dag-json $CID > ipfs_dag_get_output 2>&1 &&
test_cmp ipfs_dag_get_output curl_output
'
## Pathing, traversal
DAG_CBOR_TRAVERSAL_CID="bafyreibs4utpgbn7uqegmd2goqz4bkyflre2ek2iwv743fhvylwi4zeeim"
DAG_JSON_TRAVERSAL_CID="baguqeeram5ujjqrwheyaty3w5gdsmoz6vittchvhk723jjqxk7hakxkd47xq"
DAG_PB_CID="bafybeiegxwlgmoh2cny7qlolykdf7aq7g6dlommarldrbm7c4hbckhfcke"
test_expect_success "Add CARs for path traversal and DAG-PB representation tests" '
ipfs dag import ../t0123-gateway-json-cbor/dag-cbor-traversal.car > import_output &&
test_should_contain $DAG_CBOR_TRAVERSAL_CID import_output &&
ipfs dag import ../t0123-gateway-json-cbor/dag-json-traversal.car > import_output &&
test_should_contain $DAG_JSON_TRAVERSAL_CID import_output &&
ipfs dag import ../t0123-gateway-json-cbor/dag-pb.car > import_output &&
test_should_contain $DAG_PB_CID import_output
'
test_expect_success "GET DAG-JSON traversal returns 501 if there is path remainder" '
curl -sD - "http://127.0.0.1:$GWAY_PORT/ipfs/$DAG_JSON_TRAVERSAL_CID/foo?format=dag-json" > curl_output 2>&1 &&
test_should_contain "501 Not Implemented" curl_output &&
test_should_contain "reading IPLD Kinds other than Links (CBOR Tag 42) is not implemented" curl_output
'
test_expect_success "GET DAG-JSON traverses multiple links" '
curl -s "http://127.0.0.1:$GWAY_PORT/ipfs/$DAG_JSON_TRAVERSAL_CID/foo/link/bar?format=dag-json" > curl_output 2>&1 &&
jq --sort-keys . curl_output > actual &&
echo "{ \"hello\": \"this is not a link\" }" | jq --sort-keys . > expected &&
test_cmp expected actual
'
test_expect_success "GET DAG-CBOR traversal returns 501 if there is path remainder" '
curl -sD - "http://127.0.0.1:$GWAY_PORT/ipfs/$DAG_CBOR_TRAVERSAL_CID/foo?format=dag-cbor" > curl_output 2>&1 &&
test_should_contain "501 Not Implemented" curl_output &&
test_should_contain "reading IPLD Kinds other than Links (CBOR Tag 42) is not implemented" curl_output
'
test_expect_success "GET DAG-CBOR traverses multiple links" '
curl -s "http://127.0.0.1:$GWAY_PORT/ipfs/$DAG_CBOR_TRAVERSAL_CID/foo/link/bar?format=dag-json" > curl_output 2>&1 &&
jq --sort-keys . curl_output > actual &&
echo "{ \"hello\": \"this is not a link\" }" | jq --sort-keys . > expected &&
test_cmp expected actual
'
# test_expect_success "GET DAG-PB has expected output" '
# curl -s "http://127.0.0.1:$GWAY_PORT/ipfs/$DAG_PB_CID?format=dag-json" > curl_output 2>&1 &&
# jq --sort-keys . curl_output > actual &&
# test_cmp ../t0123-gateway-json-cbor/dag-pb.json actual
# '
## NATIVE TESTS:
## DAG- regression tests for core behaviors when native DAG-(CBOR|JSON) is requested
test_native_dag () {
name=$1
format=$2
disposition=$3
CID=$4
# GET without explicit format and Accept: text/html returns raw block
test_expect_success "GET $name from /ipfs without explicit format returns the same payload as the raw block" '
ipfs block get "/ipfs/$CID" > expected &&
curl -sX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$CID" -o curl_output &&
test_cmp expected curl_output
'
# GET dag-cbor block via Accept and ?format and ensure both are the same as `ipfs block get` output
test_expect_success "GET $name from /ipfs with format=dag-$format returns the same payload as the raw block" '
ipfs block get "/ipfs/$CID" > expected &&
curl -sX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$CID?format=dag-$format" -o curl_ipfs_dag_param_output &&
test_cmp expected curl_ipfs_dag_param_output
'
test_expect_success "GET $name from /ipfs with format=$format returns the same payload as format=dag-$format" '
curl -sX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$CID?format=dag-$format" -o expected &&
curl -sX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$CID?format=dag-$format" -o curl_ipfs_dag_param_output &&
test_cmp expected curl_ipfs_dag_param_output
'
test_expect_success "GET $name from /ipfs with application/vnd.ipld.dag-$format returns the same payload as the raw block" '
ipfs block get "/ipfs/$CID" > expected_block &&
curl -sX GET -H "Accept: application/vnd.ipld.dag-$format" "http://127.0.0.1:$GWAY_PORT/ipfs/$CID" -o curl_ipfs_dag_block_accept_output &&
test_cmp expected_block curl_ipfs_dag_block_accept_output
'
# Make sure expected HTTP headers are returned with the dag- block
test_expect_success "GET response for application/vnd.ipld.dag-$format has expected Content-Type" '
curl -svX GET -H "Accept: application/vnd.ipld.dag-$format" "http://127.0.0.1:$GWAY_PORT/ipfs/$CID" >/dev/null 2>curl_output &&
test_should_contain "< Content-Type: application/vnd.ipld.dag-$format" curl_output
'
test_expect_success "GET response for application/vnd.ipld.dag-$format includes Content-Length" '
BYTES=$(ipfs block get $CID | wc --bytes)
test_should_contain "< Content-Length: $BYTES" curl_output
'
test_expect_success "GET response for application/vnd.ipld.dag-$format includes Content-Disposition" '
test_should_contain "< Content-Disposition: ${disposition}\; filename=\"${CID}.${format}\"" curl_output
'
test_expect_success "GET response for application/vnd.ipld.dag-$format includes nosniff hint" '
test_should_contain "< X-Content-Type-Options: nosniff" curl_output
'
test_expect_success "GET for application/vnd.ipld.dag-$format with query filename includes Content-Disposition with custom filename" '
curl -svX GET -H "Accept: application/vnd.ipld.dag-$format" "http://127.0.0.1:$GWAY_PORT/ipfs/$CID?filename=foobar.$format" >/dev/null 2>curl_output_filename &&
test_should_contain "< Content-Disposition: ${disposition}\; filename=\"foobar.$format\"" curl_output_filename
'
test_expect_success "GET for application/vnd.ipld.dag-$format with ?download=true forces Content-Disposition: attachment" '
curl -svX GET -H "Accept: application/vnd.ipld.dag-$format" "http://127.0.0.1:$GWAY_PORT/ipfs/$CID?filename=foobar.$format&download=true" >/dev/null 2>curl_output_filename &&
test_should_contain "< Content-Disposition: attachment" curl_output_filename
'
# Cache control HTTP headers
# (basic checks, detailed behavior is tested in t0116-gateway-cache.sh)
test_expect_success "GET response for application/vnd.ipld.dag-$format includes Etag" '
test_should_contain "< Etag: \"${CID}.dag-$format\"" curl_output
'
test_expect_success "GET response for application/vnd.ipld.dag-$format includes X-Ipfs-Path and X-Ipfs-Roots" '
test_should_contain "< X-Ipfs-Path" curl_output &&
test_should_contain "< X-Ipfs-Roots" curl_output
'
test_expect_success "GET response for application/vnd.ipld.dag-$format includes Cache-Control" '
test_should_contain "< Cache-Control: public, max-age=29030400, immutable" curl_output
'
# HTTP HEAD behavior
test_expect_success "HEAD $name with no explicit format returns HTTP 200" '
curl -I "http://127.0.0.1:$GWAY_PORT/ipfs/$CID" -o output &&
test_should_contain "HTTP/1.1 200 OK" output &&
test_should_contain "Content-Type: application/vnd.ipld.dag-$format" output &&
test_should_contain "Content-Length: " output
'
test_expect_success "HEAD $name with an explicit JSON format returns HTTP 200" '
curl -I "http://127.0.0.1:$GWAY_PORT/ipfs/$CID?format=json" -o output &&
test_should_contain "HTTP/1.1 200 OK" output &&
test_should_contain "Etag: \"$CID.json\"" output &&
test_should_contain "Content-Type: application/json" output &&
test_should_contain "Content-Length: " output
'
test_expect_success "HEAD dag-pb with ?format=$format returns HTTP 200" '
curl -I "http://127.0.0.1:$GWAY_PORT/ipfs/$FILE_CID?format=$format" -o output &&
test_should_contain "HTTP/1.1 200 OK" output &&
test_should_contain "Etag: \"$FILE_CID.$format\"" output &&
test_should_contain "Content-Type: application/$format" output &&
test_should_contain "Content-Length: " output
'
test_expect_success "HEAD $name with only-if-cached for missing block returns HTTP 412 Precondition Failed" '
MISSING_CID=$(echo "{\"t\": \"$(date +%s)\"}" | ipfs dag put --store-codec=dag-${format}) &&
ipfs block rm -f -q $MISSING_CID &&
curl -I -H "Cache-Control: only-if-cached" "http://127.0.0.1:$GWAY_PORT/ipfs/$MISSING_CID" -o output &&
test_should_contain "HTTP/1.1 412 Precondition Failed" output
'
# IPNS behavior (should be same as immutable /ipfs, but with different caching headers)
# To keep tests small we only confirm payload is the same, and then only test delta around caching headers.
test_expect_success "Prepare IPNS with dag-$format" '
IPNS_ID=$(ipfs key gen --ipns-base=base36 --type=ed25519 ${format}_test_key | head -n1 | tr -d "\n") &&
ipfs name publish --key ${format}_test_key --allow-offline -Q "/ipfs/$CID" > name_publish_out &&
test_check_peerid "${IPNS_ID}" &&
ipfs name resolve "${IPNS_ID}" > output &&
printf "/ipfs/%s\n" "$CID" > expected &&
test_cmp expected output
'
test_expect_success "GET $name from /ipns without explicit format returns the same payload as /ipfs" '
curl -sX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$CID" -o ipfs_output &&
curl -sX GET "http://127.0.0.1:$GWAY_PORT/ipns/$IPNS_ID" -o ipns_output &&
test_cmp ipfs_output ipns_output
'
test_expect_success "GET $name from /ipns without explicit format returns the same payload as /ipfs" '
curl -sX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$CID?format=dag-$format" -o ipfs_output &&
curl -sX GET "http://127.0.0.1:$GWAY_PORT/ipns/$IPNS_ID?format=dag-$format" -o ipns_output &&
test_cmp ipfs_output ipns_output
'
test_expect_success "GET $name from /ipns with explicit application/vnd.ipld.dag-$format has expected headers" '
curl -svX GET -H "Accept: application/vnd.ipld.dag-$format" "http://127.0.0.1:$GWAY_PORT/ipns/$IPNS_ID" >/dev/null 2>curl_output &&
test_should_not_contain "Cache-Control" curl_output &&
test_should_contain "< Content-Type: application/vnd.ipld.dag-$format" curl_output &&
test_should_contain "< Etag: \"${CID}.dag-$format\"" curl_output &&
test_should_contain "< X-Ipfs-Path" curl_output &&
test_should_contain "< X-Ipfs-Roots" curl_output
'
# When Accept header includes text/html and no explicit format is requested for DAG-(CBOR|JSON)
# The gateway returns generated HTML index (see dag-index-html) for web browsers (similar to dir-index-html returned for unixfs dirs)
# As this is generated, we don't return immutable Cache-Control, even on /ipfs (same as for dir-index-html)
test_expect_success "GET $name on /ipfs with Accept: text/html returns HTML (dag-index-html)" '
curl -sD - -H "Accept: text/html" "http://127.0.0.1:$GWAY_PORT/ipfs/$CID" > curl_output 2>&1 &&
test_should_not_contain "Content-Disposition" curl_output &&
test_should_not_contain "Cache-Control" curl_output &&
test_should_contain "Etag: \"DagIndex-" curl_output &&
test_should_contain "Content-Type: text/html" curl_output &&
test_should_contain "</html>" curl_output
'
test_expect_success "GET $name on /ipns with Accept: text/html returns HTML (dag-index-html)" '
curl -sD - -H "Accept: text/html" "http://127.0.0.1:$GWAY_PORT/ipns/$IPNS_ID" > curl_output 2>&1 &&
test_should_not_contain "Content-Disposition" curl_output &&
test_should_not_contain "Cache-Control" curl_output &&
test_should_contain "Etag: \"DagIndex-" curl_output &&
test_should_contain "Content-Type: text/html" curl_output &&
test_should_contain "</html>" curl_output
'
}
test_native_dag "DAG-JSON" "json" "inline" "$DAG_JSON_TRAVERSAL_CID"
test_native_dag "DAG-CBOR" "cbor" "attachment" "$DAG_CBOR_TRAVERSAL_CID"
test_kill_ipfs_daemon
test_done
# vim: set ts=2 sw=2 et:

Binary file not shown.

View File

@ -0,0 +1,23 @@
{
"Data": {
"/": {
"bytes": "CAE"
}
},
"Links": [
{
"Hash": {
"/": "bafybeidryarwh34ygbtyypbu7qjkl4euiwxby6cql6uvosonohkq2kwnkm"
},
"Name": "foo",
"Tsize": 69
},
{
"Hash": {
"/": "bafkreic3ondyhizrzeoufvoodehinugpj3ecruwokaygl7elezhn2khqfa"
},
"Name": "foo.txt",
"Tsize": 13
}
]
}