generated from ipfs/ipfs-repository-template
-
Notifications
You must be signed in to change notification settings - Fork 75
/
handler_codec.go
334 lines (284 loc) · 12 KB
/
handler_codec.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
package gateway
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/ipfs/boxo/gateway/assets"
"github.com/ipfs/boxo/path"
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime/multicodec"
"github.com/ipld/go-ipld-prime/node/basicnode"
mc "github.com/multiformats/go-multicodec"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
// Ensure basic codecs are registered.
_ "github.com/ipld/go-ipld-prime/codec/cbor"
_ "github.com/ipld/go-ipld-prime/codec/dagcbor"
_ "github.com/ipld/go-ipld-prime/codec/dagjson"
_ "github.com/ipld/go-ipld-prime/codec/json"
)
// codecToContentType maps the supported IPLD codecs to the HTTP Content
// Type they should have.
var codecToContentType = map[mc.Code]string{
mc.Json: jsonResponseFormat,
mc.Cbor: cborResponseFormat,
mc.DagJson: dagJsonResponseFormat,
mc.DagCbor: dagCborResponseFormat,
}
// contentTypeToRaw maps the HTTP Content Type to the respective codec that
// allows raw response without any conversion.
var contentTypeToRaw = map[string][]mc.Code{
jsonResponseFormat: {mc.Json, mc.DagJson},
cborResponseFormat: {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{
dagJsonResponseFormat: mc.DagJson,
dagCborResponseFormat: 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{
jsonResponseFormat: ".json",
dagJsonResponseFormat: ".json",
cborResponseFormat: ".cbor",
dagCborResponseFormat: ".cbor",
}
func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, rq *requestData) bool {
ctx, span := spanTrace(ctx, "Handler.ServeCodec", trace.WithAttributes(attribute.String("path", rq.immutablePath.String()), attribute.String("requestedContentType", rq.responseFormat)))
defer span.End()
pathMetadata, data, err := i.backend.GetBlock(ctx, rq.mostlyResolvedPath())
if !i.handleRequestErrors(w, r, rq.contentPath, err) {
return false
}
defer data.Close()
setIpfsRootsHeader(w, rq, &pathMetadata)
blockSize, err := data.Size()
if !i.handleRequestErrors(w, r, rq.contentPath, err) {
return false
}
return i.renderCodec(ctx, w, r, rq, blockSize, data)
}
func (i *handler) renderCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, rq *requestData, blockSize int64, blockData io.ReadSeekCloser) bool {
resolvedPath := rq.pathMetadata.LastSegment
ctx, span := spanTrace(ctx, "Handler.RenderCodec", trace.WithAttributes(attribute.String("path", resolvedPath.String()), attribute.String("requestedContentType", rq.responseFormat)))
defer span.End()
blockCid := resolvedPath.RootCid()
cidCodec := mc.Code(blockCid.Prefix().Codec)
responseContentType := rq.responseFormat
// 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 len(rq.pathMetadata.LastSegmentRemainder) != 0 {
remainderStr := path.SegmentsToString(rq.pathMetadata.LastSegmentRemainder...)
path := strings.TrimSuffix(resolvedPath.String(), remainderStr)
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", remainderStr, resolvedPath.String(), path)
i.webError(w, r, err, http.StatusNotImplemented)
return false
}
// If no explicit content type was requested, the response will have one based on the codec from the CID
if rq.responseFormat == "" {
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)
i.webError(w, r, err, http.StatusInternalServerError)
return false
}
responseContentType = cidContentType
}
// Set HTTP headers (for caching, etc). Etag will be replaced if handled by serveCodecHTML.
modtime := addCacheControlHeaders(w, r, rq.contentPath, rq.ttl, rq.lastMod, resolvedPath.RootCid(), responseContentType)
_ = 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 rq.responseFormat == "" {
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 {
return i.serveCodecHTML(ctx, w, r, blockCid, blockData, resolvedPath, rq.contentPath)
} else {
// This covers CIDs with codec 'json' and 'cbor' as those do not have
// an explicit requested content type.
return i.serveCodecRaw(ctx, w, r, blockSize, blockData, rq.contentPath, modtime, rq.begin)
}
}
// If DAG-JSON or DAG-CBOR was requested using corresponding plain content type
// return raw block as-is, without conversion
skipCodecs, ok := contentTypeToRaw[rq.responseFormat]
if ok {
for _, skipCodec := range skipCodecs {
if skipCodec == cidCodec {
return i.serveCodecRaw(ctx, w, r, blockSize, blockData, rq.contentPath, modtime, rq.begin)
}
}
}
// 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[rq.responseFormat]
if !ok {
err := fmt.Errorf("converting from %q to %q is not supported", cidCodec.String(), rq.responseFormat)
i.webError(w, r, err, http.StatusBadRequest)
return false
}
// This handles DAG-* conversions and validations.
return i.serveCodecConverted(ctx, w, r, blockCid, blockData, rq.contentPath, toCodec, modtime, rq.begin)
}
func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r *http.Request, blockCid cid.Cid, blockData io.Reader, resolvedPath path.ImmutablePath, contentPath path.Path) bool {
// WithHostname may have constructed an IPFS (or IPNS) path using the Host header.
// In this case, we need the original path for constructing the redirect.
requestURI, err := url.ParseRequestURI(r.RequestURI)
if err != nil {
i.webError(w, r, fmt.Errorf("failed to parse request path: %w", err), http.StatusInternalServerError)
return false
}
// Ensure HTML rendering is in a path that ends with trailing slash.
if requestURI.Path[len(requestURI.Path)-1] != '/' {
suffix := "/"
// preserve query parameters
if r.URL.RawQuery != "" {
suffix = suffix + "?" + r.URL.RawQuery
}
// /ipfs/cid/foo?bar must be redirected to /ipfs/cid/foo/?bar
redirectURL := requestURI.Path + suffix
http.Redirect(w, r, redirectURL, http.StatusMovedPermanently)
return true
}
// 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.RootCid())
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.RootCid().Prefix().Codec)
err = assets.DagTemplate.Execute(w, assets.DagTemplateData{
GlobalData: i.getTemplateGlobalData(r, contentPath),
Path: contentPath.String(),
CID: resolvedPath.RootCid().String(),
CodecName: cidCodec.String(),
CodecHex: fmt.Sprintf("0x%x", uint64(cidCodec)),
Node: parseNode(blockCid, blockData),
})
if err != nil {
_, _ = w.Write([]byte(fmt.Sprintf("error during body generation: %v", err)))
}
return err == nil
}
// parseNode does a best effort attempt to parse this request's block such that
// a preview can be displayed in the gateway. If something fails along the way,
// returns nil, therefore not displaying the preview.
func parseNode(blockCid cid.Cid, blockData io.Reader) *assets.ParsedNode {
codec := blockCid.Prefix().Codec
decoder, err := multicodec.LookupDecoder(codec)
if err != nil {
return nil
}
nodeBuilder := basicnode.Prototype.Any.NewBuilder()
err = decoder(nodeBuilder, blockData)
if err != nil {
return nil
}
parsedNode, err := assets.ParseNode(nodeBuilder.Build())
if err != nil {
return nil
}
return parsedNode
}
// serveCodecRaw returns the raw block without any conversion
func (i *handler) serveCodecRaw(ctx context.Context, w http.ResponseWriter, r *http.Request, blockSize int64, blockData io.ReadSeekCloser, contentPath path.Path, modtime, begin time.Time) bool {
// ServeContent will take care of
// If-None-Match+Etag, Content-Length and setting range request headers after we've already seeked to the start of
// the first range
if !i.seekToStartOfFirstRange(w, r, blockData) {
return false
}
_, dataSent, _ := serveContent(w, r, modtime, blockSize, blockData)
if dataSent {
// Update metrics
i.jsoncborDocumentGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
}
return dataSent
}
// serveCodecConverted returns payload converted to codec specified in toCodec
func (i *handler) serveCodecConverted(ctx context.Context, w http.ResponseWriter, r *http.Request, blockCid cid.Cid, blockData io.ReadCloser, contentPath path.Path, toCodec mc.Code, modtime, begin time.Time) bool {
codec := blockCid.Prefix().Codec
decoder, err := multicodec.LookupDecoder(codec)
if err != nil {
i.webError(w, r, err, http.StatusInternalServerError)
return false
}
node := basicnode.Prototype.Any.NewBuilder()
err = decoder(node, blockData)
if err != nil {
i.webError(w, r, err, http.StatusInternalServerError)
return false
}
encoder, err := multicodec.LookupEncoder(uint64(toCodec))
if err != nil {
i.webError(w, r, err, http.StatusInternalServerError)
return false
}
// Ensure IPLD node conforms to the codec specification.
var buf bytes.Buffer
err = encoder(node.Build(), &buf)
if err != nil {
i.webError(w, r, err, http.StatusInternalServerError)
return false
}
// 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))
}
_, err = w.Write(buf.Bytes())
if err == nil {
// Update metrics
i.jsoncborDocumentGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
return true
}
return false
}
func setCodecContentDisposition(w http.ResponseWriter, r *http.Request, resolvedPath path.ImmutablePath, 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.RootCid().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() + `"`
}