5
"fmt"
6
"html/template"
7
"io"
8
- "mime"
8
"net/http"
9
"net/url"
10
"os"
15
"strings"
16
"time"
17
19
- humanize "github.com/dustin/go-humanize"
20
- "github.com/gabriel-vasile/mimetype"
21
- "github.com/ipfs/go-cid"
18
+ cid "github.com/ipfs/go-cid"
19
files "github.com/ipfs/go-ipfs-files"
23
- assets "github.com/ipfs/go-ipfs/assets"
20
dag "github.com/ipfs/go-merkledag"
21
mfs "github.com/ipfs/go-mfs"
22
path "github.com/ipfs/go-path"
28
)
29
30
const (
35
- ipfsPathPrefix = "/ipfs/"
36
- ipnsPathPrefix = "/ipns/"
31
+ ipfsPathPrefix = "/ipfs/"
32
+ ipnsPathPrefix = "/ipns/"
33
+ immutableCacheControl = "public, max-age=29030400, immutable"
34
)
35
36
var onlyAscii = regexp.MustCompile("[[:^ascii:]]")
37
+var noModtime = time.Unix(0, 0) // disables Last-Modified header if passed as modtime
38
39
// HTML-based redirect for errors which can be recovered from, but we want
40
// to provide hint to people that they should fix things on their end.
87
88
func newGatewayHandler(c GatewayConfig, api coreiface.CoreAPI) *gatewayHandler {
89
unixfsGetMetric := prometheus.NewSummaryVec(
90
+ // TODO: deprecate and switch to content type agnostic metrics: https://github.com/ipfs/go-ipfs/issues/8441
91
prometheus.SummaryOpts{
92
Namespace: "ipfs",
93
Subsystem: "http",
195
196
func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) {
197
begin := time.Now()
199
- urlPath := r.URL.Path
200
- escapedURLPath := r.URL.EscapedPath()
198
199
logger := log.With("from", r.RequestURI)
200
logger.Debug("http request received")
201
205
- // If the gateway is behind a reverse proxy and mounted at a sub-path,
206
- // the prefix header can be set to signal this sub-path.
207
- // It will be prepended to links in directory listings and the index.html redirect.
208
- // TODO: this feature is deprecated and will be removed (https://github.com/ipfs/go-ipfs/issues/7702)
209
- prefix := ""
210
- if prfx := r.Header.Get("X-Ipfs-Gateway-Prefix"); len(prfx) > 0 {
211
- for _, p := range i.config.PathPrefixes {
212
- if prfx == p || strings.HasPrefix(prfx, p+"/") {
213
- prefix = prfx
214
- break
215
- }
216
- }
217
- logger.Debugw("sub-path (deprecrated)", "prefix", prefix)
218
- }
219
-
220
- // HostnameOption might have constructed an IPNS/IPFS path using the Host header.
221
- // In this case, we need the original path for constructing redirects
222
- // and links that match the requested URL.
223
- // For example, http://example.net would become /ipns/example.net, and
224
- // the redirects and links would end up as http://example.net/ipns/example.net
225
- requestURI, err := url.ParseRequestURI(r.RequestURI)
226
- if err != nil {
227
- webError(w, "failed to parse request path", err, http.StatusInternalServerError)
202
+ // X-Ipfs-Gateway-Prefix was removed (https://github.com/ipfs/go-ipfs/issues/7702)
203
+ // TODO: remove this after go-ipfs 0.13 ships
204
+ if prfx := r.Header.Get("X-Ipfs-Gateway-Prefix"); prfx != "" {
205
+ err := fmt.Errorf("X-Ipfs-Gateway-Prefix support was removed: https://github.com/ipfs/go-ipfs/issues/7702")
206
+ webError(w, "unsupported HTTP header", err, http.StatusBadRequest)
207
return
208
}
230
- originalUrlPath := prefix + requestURI.Path
209
210
// ?uri query param support for requests produced by web browsers
211
// via navigator.registerProtocolHandler Web API
226
path = path + "?" + u.RawQuery
227
}
228
251
- redirectURL := gopath.Join("/", prefix, u.Scheme, u.Host, path)
229
+ redirectURL := gopath.Join("/", u.Scheme, u.Host, path)
230
logger.Debugw("uri param, redirect", "to", redirectURL, "status", http.StatusMovedPermanently)
231
http.Redirect(w, r, redirectURL, http.StatusMovedPermanently)
232
return
244
}
245
}
246
269
- parsedPath := ipath.New(urlPath)
270
- if pathErr := parsedPath.IsValid(); pathErr != nil {
271
- if prefix == "" && fixupSuperfluousNamespace(w, urlPath, r.URL.RawQuery) {
247
+ contentPath := ipath.New(r.URL.Path)
248
+ if pathErr := contentPath.IsValid(); pathErr != nil {
249
+ if fixupSuperfluousNamespace(w, r.URL.Path, r.URL.RawQuery) {
250
// the error was due to redundant namespace, which we were able to fix
251
// by returning error/redirect page, nothing left to do here
252
logger.Debugw("redundant namespace; noop")
258
}
259
260
// Resolve path to the final DAG node for the ETag
283
- resolvedPath, err := i.api.ResolvePath(r.Context(), parsedPath)
261
+ resolvedPath, err := i.api.ResolvePath(r.Context(), contentPath)
262
switch err {
263
case nil:
264
case coreiface.ErrOffline:
287
- webError(w, "ipfs resolve -r "+escapedURLPath, err, http.StatusServiceUnavailable)
265
+ webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusServiceUnavailable)
266
return
267
default:
290
- if i.servePretty404IfPresent(w, r, parsedPath) {
268
+ // if Accept is text/html, see if ipfs-404.html is present
269
+ if i.servePretty404IfPresent(w, r, contentPath) {
270
logger.Debugw("serve pretty 404 if present")
271
return
272
}
273
295
- webError(w, "ipfs resolve -r "+escapedURLPath, err, http.StatusNotFound)
296
- return
297
- }
298
-
299
- dr, err := i.api.Unixfs().Get(r.Context(), resolvedPath)
300
- if err != nil {
301
- webError(w, "ipfs cat "+escapedURLPath, err, http.StatusNotFound)
274
+ webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusNotFound)
275
return
276
}
277
305
- i.unixfsGetMetric.WithLabelValues(parsedPath.Namespace()).Observe(time.Since(begin).Seconds())
306
-
307
- defer dr.Close()
308
-
309
- var responseEtag string
278
+ // Detect when explicit Accept header or ?format parameter are present
279
+ responseFormat := customResponseFormat(r)
280
311
- // we need to figure out whether this is a directory before doing most of the heavy lifting below
312
- _, ok := dr.(files.Directory)
313
-
314
- if ok && assets.BindataVersionHash != "" {
315
- responseEtag = `"DirIndex-` + assets.BindataVersionHash + `_CID-` + resolvedPath.Cid().String() + `"`
316
- } else {
317
- responseEtag = `"` + resolvedPath.Cid().String() + `"`
281
+ // Finish early if client already has matching Etag
282
+ if r.Header.Get("If-None-Match") == getEtag(r, resolvedPath.Cid()) {
283
+ w.WriteHeader(http.StatusNotModified)
284
+ return
285
}
286
320
- // Check etag sent back to us
321
- if r.Header.Get("If-None-Match") == responseEtag || r.Header.Get("If-None-Match") == `W/`+responseEtag {
322
- w.WriteHeader(http.StatusNotModified)
287
+ // Update the global metric of the time it takes to read the final root block of the requested resource
288
+ // NOTE: for legacy reasons this happens before we go into content-type specific code paths
289
+ _, err = i.api.Block().Get(r.Context(), resolvedPath)
290
+ if err != nil {
291
+ webError(w, "ipfs block get "+resolvedPath.Cid().String(), err, http.StatusInternalServerError)
292
return
293
}
294
+ i.unixfsGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
295
296
+ // HTTP Headers
297
i.addUserHeaders(w) // ok, _now_ write user's headers.
327
- w.Header().Set("X-IPFS-Path", urlPath)
328
- w.Header().Set("Etag", responseEtag)
298
+ w.Header().Set("X-Ipfs-Path", contentPath.String())
299
330
- if rootCids, err := i.buildIpfsRootsHeader(urlPath, r); err == nil {
300
+ if rootCids, err := i.buildIpfsRootsHeader(contentPath.String(), r); err == nil {
301
w.Header().Set("X-Ipfs-Roots", rootCids)
332
- } else { // this should never happen, as we resolved the urlPath already
302
+ } else { // this should never happen, as we resolved the contentPath already
303
webError(w, "error while resolving X-Ipfs-Roots", err, http.StatusInternalServerError)
304
return
305
}
306
337
- // set these headers _after_ the error, for we may just not have it
338
- // and don't want the client to cache a 500 response...
339
- // and only if it's /ipfs!
340
- // TODO: break this out when we split /ipfs /ipns routes.
341
- modtime := time.Now()
342
-
343
- if f, ok := dr.(files.File); ok {
344
- if strings.HasPrefix(urlPath, ipfsPathPrefix) {
345
- w.Header().Set("Cache-Control", "public, max-age=29030400, immutable")
346
-
347
- // set modtime to a really long time ago, since files are immutable and should stay cached
348
- modtime = time.Unix(1, 0)
349
- }
350
-
351
- urlFilename := r.URL.Query().Get("filename")
352
- var name string
353
- if urlFilename != "" {
354
- disposition := "inline"
355
- if r.URL.Query().Get("download") == "true" {
356
- disposition = "attachment"
357
- }
358
- utf8Name := url.PathEscape(urlFilename)
359
- asciiName := url.PathEscape(onlyAscii.ReplaceAllLiteralString(urlFilename, "_"))
360
- w.Header().Set("Content-Disposition", fmt.Sprintf("%s; filename=\"%s\"; filename*=UTF-8''%s", disposition, asciiName, utf8Name))
361
- name = urlFilename
362
- } else {
363
- name = getFilename(urlPath)
364
- }
365
-
366
- logger.Debugw("serving file", "name", name)
367
- i.serveFile(w, r, name, modtime, f)
307
+ // Support custom response formats passed via ?format or Accept HTTP header
308
+ switch responseFormat {
309
+ case "": // The implicit response format is UnixFS
310
+ logger.Debugw("serving unixfs", "path", contentPath)
311
+ i.serveUnixFs(w, r, resolvedPath, contentPath, logger)
312
return
369
- }
370
- dir, ok := dr.(files.Directory)
371
- if !ok {
372
- internalWebError(w, fmt.Errorf("unsupported file type"))
313
+ case "application/vnd.ipld.raw":
314
+ logger.Debugw("serving raw block", "path", contentPath)
315
+ i.serveRawBlock(w, r, resolvedPath.Cid(), contentPath)
316
return
374
- }
375
-
376
- idxPath := ipath.Join(resolvedPath, "index.html")
377
- idx, err := i.api.Unixfs().Get(r.Context(), idxPath)
378
- switch err.(type) {
379
- case nil:
380
- dirwithoutslash := urlPath[len(urlPath)-1] != '/'
381
- goget := r.URL.Query().Get("go-get") == "1"
382
- if dirwithoutslash && !goget {
383
- // See comment above where originalUrlPath is declared.
384
- suffix := "/"
385
- if r.URL.RawQuery != "" {
386
- // preserve query parameters
387
- suffix = suffix + "?" + r.URL.RawQuery
388
- }
389
-
390
- redirectURL := originalUrlPath + suffix
391
- logger.Debugw("serving index.html file", "to", redirectURL, "status", http.StatusFound, "path", idxPath)
392
- http.Redirect(w, r, redirectURL, http.StatusFound)
393
- return
394
- }
395
-
396
- f, ok := idx.(files.File)
397
- if !ok {
398
- internalWebError(w, files.ErrNotReader)
399
- return
400
- }
401
- // static index.html → no need to generate dynamic dir-index-html
402
- // replace mutable DirIndex Etag with immutable dir CID
403
- w.Header().Set("Etag", `"`+resolvedPath.Cid().String()+`"`)
404
-
405
- logger.Debugw("serving index.html file", "path", idxPath)
406
- // write to request
407
- i.serveFile(w, r, "index.html", modtime, f)
408
- return
409
- case resolver.ErrNoLink:
410
- logger.Debugw("no index.html; noop", "path", idxPath)
411
- default:
412
- internalWebError(w, err)
413
- return
414
- }
415
-
416
- // See statusResponseWriter.WriteHeader
417
- // and https://github.com/ipfs/go-ipfs/issues/7164
418
- // Note: this needs to occur before listingTemplate.Execute otherwise we get
419
- // superfluous response.WriteHeader call from prometheus/client_golang
420
- if w.Header().Get("Location") != "" {
421
- logger.Debugw("location moved permanently", "status", http.StatusMovedPermanently)
422
- w.WriteHeader(http.StatusMovedPermanently)
317
+ case "application/vnd.ipld.car", "application/vnd.ipld.car; version=1":
318
+ logger.Debugw("serving car stream", "path", contentPath)
319
+ i.serveCar(w, r, resolvedPath.Cid(), contentPath)
320
return
424
- }
425
-
426
- // A HTML directory index will be presented, be sure to set the correct
427
- // type instead of relying on autodetection (which may fail).
428
- w.Header().Set("Content-Type", "text/html")
429
- if r.Method == http.MethodHead {
430
- logger.Debug("return as request's HTTP method is HEAD")
321
+ default: // catch-all for unsuported application/vnd.*
322
+ err := fmt.Errorf("unsupported format %q", responseFormat)
323
+ webError(w, "failed respond with requested content type", err, http.StatusBadRequest)
324
return
325
}
433
-
434
- // storage for directory listing
435
- var dirListing []directoryItem
436
- dirit := dir.Entries()
437
- for dirit.Next() {
438
- size := "?"
439
- if s, err := dirit.Node().Size(); err == nil {
440
- // Size may not be defined/supported. Continue anyways.
441
- size = humanize.Bytes(uint64(s))
442
- }
443
-
444
- resolved, err := i.api.ResolvePath(r.Context(), ipath.Join(resolvedPath, dirit.Name()))
445
- if err != nil {
446
- internalWebError(w, err)
447
- return
448
- }
449
- hash := resolved.Cid().String()
450
-
451
- // See comment above where originalUrlPath is declared.
452
- di := directoryItem{
453
- Size: size,
454
- Name: dirit.Name(),
455
- Path: gopath.Join(originalUrlPath, dirit.Name()),
456
- Hash: hash,
457
- ShortHash: shortHash(hash),
458
- }
459
- dirListing = append(dirListing, di)
460
- }
461
- if dirit.Err() != nil {
462
- internalWebError(w, dirit.Err())
463
- return
464
- }
465
-
466
- // construct the correct back link
467
- // https://github.com/ipfs/go-ipfs/issues/1365
468
- var backLink string = originalUrlPath
469
-
470
- // don't go further up than /ipfs/$hash/
471
- pathSplit := path.SplitList(urlPath)
472
- switch {
473
- // keep backlink
474
- case len(pathSplit) == 3: // url: /ipfs/$hash
475
-
476
- // keep backlink
477
- case len(pathSplit) == 4 && pathSplit[3] == "": // url: /ipfs/$hash/
478
-
479
- // add the correct link depending on whether the path ends with a slash
480
- default:
481
- if strings.HasSuffix(backLink, "/") {
482
- backLink += "./.."
483
- } else {
484
- backLink += "/.."
485
- }
486
- }
487
-
488
- size := "?"
489
- if s, err := dir.Size(); err == nil {
490
- // Size may not be defined/supported. Continue anyways.
491
- size = humanize.Bytes(uint64(s))
492
- }
493
-
494
- hash := resolvedPath.Cid().String()
495
-
496
- // Gateway root URL to be used when linking to other rootIDs.
497
- // This will be blank unless subdomain or DNSLink resolution is being used
498
- // for this request.
499
- var gwURL string
500
-
501
- // Get gateway hostname and build gateway URL.
502
- if h, ok := r.Context().Value("gw-hostname").(string); ok {
503
- gwURL = "//" + h
504
- } else {
505
- gwURL = ""
506
- }
507
-
508
- dnslink := hasDNSLinkOrigin(gwURL, urlPath)
509
-
510
- // See comment above where originalUrlPath is declared.
511
- tplData := listingTemplateData{
512
- GatewayURL: gwURL,
513
- DNSLink: dnslink,
514
- Listing: dirListing,
515
- Size: size,
516
- Path: urlPath,
517
- Breadcrumbs: breadcrumbs(urlPath, dnslink),
518
- BackLink: backLink,
519
- Hash: hash,
520
- }
521
-
522
- logger.Debugw("request processed", "tplDataDNSLink", dnslink, "tplDataSize", size, "tplDataBackLink", backLink, "tplDataHash", hash, "duration", time.Since(begin))
523
-
524
- if err := listingTemplate.Execute(w, tplData); err != nil {
525
- internalWebError(w, err)
526
- return
527
- }
528
-}
529
-
530
-func (i *gatewayHandler) serveFile(w http.ResponseWriter, req *http.Request, name string, modtime time.Time, file files.File) {
531
- size, err := file.Size()
532
- if err != nil {
533
- http.Error(w, "cannot serve files with unknown sizes", http.StatusBadGateway)
534
- return
535
- }
536
-
537
- content := &lazySeeker{
538
- size: size,
539
- reader: file,
540
- }
541
-
542
- var ctype string
543
- if _, isSymlink := file.(*files.Symlink); isSymlink {
544
- // We should be smarter about resolving symlinks but this is the
545
- // "most correct" we can be without doing that.
546
- ctype = "inode/symlink"
547
- } else {
548
- ctype = mime.TypeByExtension(gopath.Ext(name))
549
- if ctype == "" {
550
- // uses https://github.com/gabriel-vasile/mimetype library to determine the content type.
551
- // Fixes https://github.com/ipfs/go-ipfs/issues/7252
552
- mimeType, err := mimetype.DetectReader(content)
553
- if err != nil {
554
- http.Error(w, fmt.Sprintf("cannot detect content-type: %s", err.Error()), http.StatusInternalServerError)
555
- return
556
- }
557
-
558
- ctype = mimeType.String()
559
- _, err = content.Seek(0, io.SeekStart)
560
- if err != nil {
561
- http.Error(w, "seeker can't seek", http.StatusInternalServerError)
562
- return
563
- }
564
- }
565
- // Strip the encoding from the HTML Content-Type header and let the
566
- // browser figure it out.
567
- //
568
- // Fixes https://github.com/ipfs/go-ipfs/issues/2203
569
- if strings.HasPrefix(ctype, "text/html;") {
570
- ctype = "text/html"
571
- }
572
- }
573
- w.Header().Set("Content-Type", ctype)
574
-
575
- w = &statusResponseWriter{w}
576
- http.ServeContent(w, req, name, modtime, content)
326
}
327
579
-func (i *gatewayHandler) servePretty404IfPresent(w http.ResponseWriter, r *http.Request, parsedPath ipath.Path) bool {
580
- resolved404Path, ctype, err := i.searchUpTreeFor404(r, parsedPath)
328
+func (i *gatewayHandler) servePretty404IfPresent(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool {
329
+ resolved404Path, ctype, err := i.searchUpTreeFor404(r, contentPath)
330
if err != nil {
331
return false
332
}
347
return false
348
}
349
601
- log.Debugw("using pretty 404 file", "path", parsedPath)
350
+ log.Debugw("using pretty 404 file", "path", contentPath)
351
w.Header().Set("Content-Type", ctype)
352
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
353
w.WriteHeader(http.StatusNotFound)
544
}
545
}
546
547
+func addCacheControlHeaders(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, fileCid cid.Cid) (modtime time.Time) {
548
+ // Set Etag to based on CID (override whatever was set before)
549
+ w.Header().Set("Etag", getEtag(r, fileCid))
550
+
551
+ // Set Cache-Control and Last-Modified based on contentPath properties
552
+ if contentPath.Mutable() {
553
+ // mutable namespaces such as /ipns/ can't be cached forever
554
+
555
+ /* For now we set Last-Modified to Now() to leverage caching heuristics built into modern browsers:
556
+ * https://github.com/ipfs/go-ipfs/pull/8074#pullrequestreview-645196768
557
+ * but we should not set it to fake values and use Cache-Control based on TTL instead */
558
+ modtime = time.Now()
559
+
560
+ // TODO: set Cache-Control based on TTL of IPNS/DNSLink: https://github.com/ipfs/go-ipfs/issues/1818#issuecomment-1015849462
561
+ // TODO: set Last-Modified based on /ipns/ publishing timestamp?
562
+
563
+ } else {
564
+ // immutable! CACHE ALL THE THINGS, FOREVER! wolololol
565
+ w.Header().Set("Cache-Control", immutableCacheControl)
566
+
567
+ // Set modtime to 'zero time' to disable Last-Modified header (superseded by Cache-Control)
568
+ modtime = noModtime
569
+
570
+ // TODO: set Last-Modified? - TBD - /ipfs/ modification metadata is present in unixfs 1.5 https://github.com/ipfs/go-ipfs/issues/6920?
571
+ }
572
+
573
+ return modtime
574
+}
575
+
576
+// Set Content-Disposition if filename URL query param is present, return preferred filename
577
+func addContentDispositionHeader(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) string {
578
+ /* This logic enables:
579
+ * - creation of HTML links that trigger "Save As.." dialog instead of being rendered by the browser
580
+ * - overriding the filename used when saving subresource assets on HTML page
581
+ * - providing a default filename for HTTP clients when downloading direct /ipfs/CID without any subpath
582
+ */
583
+
584
+ // URL param ?filename=cat.jpg triggers Content-Disposition: [..] filename
585
+ // which impacts default name used in "Save As.." dialog
586
+ name := getFilename(contentPath)
587
+ urlFilename := r.URL.Query().Get("filename")
588
+ if urlFilename != "" {
589
+ disposition := "inline"
590
+ // URL param ?download=true triggers Content-Disposition: [..] attachment
591
+ // which skips rendering and forces "Save As.." dialog in browsers
592
+ if r.URL.Query().Get("download") == "true" {
593
+ disposition = "attachment"
594
+ }
595
+ setContentDispositionHeader(w, urlFilename, disposition)
596
+ name = urlFilename
597
+ }
598
+ return name
599
+}
600
+
601
+// Set Content-Disposition to arbitrary filename and disposition
602
+func setContentDispositionHeader(w http.ResponseWriter, filename string, disposition string) {
603
+ utf8Name := url.PathEscape(filename)
604
+ asciiName := url.PathEscape(onlyAscii.ReplaceAllLiteralString(filename, "_"))
605
+ w.Header().Set("Content-Disposition", fmt.Sprintf("%s; filename=\"%s\"; filename*=UTF-8''%s", disposition, asciiName, utf8Name))
606
+}
607
+
608
// Set X-Ipfs-Roots with logical CID array for efficient HTTP cache invalidation.
609
func (i *gatewayHandler) buildIpfsRootsHeader(contentPath string, r *http.Request) (string, error) {
610
/*
664
func webErrorWithCode(w http.ResponseWriter, message string, err error, code int) {
665
http.Error(w, fmt.Sprintf("%s: %s", message, err), code)
666
if code >= 500 {
857
- log.Warnf("server error: %s: %s", err)
667
+ log.Warnf("server error: %s: %s", message, err)
668
}
669
}
670
673
webErrorWithCode(w, "internalWebError", err, http.StatusInternalServerError)
674
}
675
866
-func getFilename(s string) string {
676
+func getFilename(contentPath ipath.Path) string {
677
+ s := contentPath.String()
678
if (strings.HasPrefix(s, ipfsPathPrefix) || strings.HasPrefix(s, ipnsPathPrefix)) && strings.Count(gopath.Clean(s), "/") <= 2 {
679
// Don't want to treat ipfs.io in /ipns/ipfs.io as a filename.
680
return ""
682
return gopath.Base(s)
683
}
684
874
-func (i *gatewayHandler) searchUpTreeFor404(r *http.Request, parsedPath ipath.Path) (ipath.Resolved, string, error) {
685
+// generate Etag value based on HTTP request and CID
686
+func getEtag(r *http.Request, cid cid.Cid) string {
687
+ prefix := `"`
688
+ suffix := `"`
689
+ responseFormat := customResponseFormat(r)
690
+ if responseFormat != "" {
691
+ // application/vnd.ipld.foo → foo
692
+ f := responseFormat[strings.LastIndex(responseFormat, ".")+1:]
693
+ // Etag: "cid.foo" (gives us nice compression together with Content-Disposition in block (raw) and car responses)
694
+ suffix = `.` + f + suffix
695
+ }
696
+ // TODO: include selector suffix when https://github.com/ipfs/go-ipfs/issues/8769 lands
697
+ return prefix + cid.String() + suffix
698
+}
699
+
700
+// return explicit response format if specified in request as query parameter or via Accept HTTP header
701
+func customResponseFormat(r *http.Request) string {
702
+ if formatParam := r.URL.Query().Get("format"); formatParam != "" {
703
+ // translate query param to a content type
704
+ switch formatParam {
705
+ case "raw":
706
+ return "application/vnd.ipld.raw"
707
+ case "car":
708
+ return "application/vnd.ipld.car"
709
+ }
710
+ }
711
+ // Browsers and other user agents will send Accept header with generic types like:
712
+ // Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
713
+ // We only care about explciit, vendor-specific content-types.
714
+ for _, accept := range r.Header.Values("Accept") {
715
+ // respond to the very first ipld content type
716
+ if strings.HasPrefix(accept, "application/vnd.ipld") {
717
+ return accept
718
+ }
719
+ }
720
+ return ""
721
+}
722
+
723
+func (i *gatewayHandler) searchUpTreeFor404(r *http.Request, contentPath ipath.Path) (ipath.Resolved, string, error) {
724
filename404, ctype, err := preferred404Filename(r.Header.Values("Accept"))
725
if err != nil {
726
return nil, "", err
727
}
728
880
- pathComponents := strings.Split(parsedPath.String(), "/")
729
+ pathComponents := strings.Split(contentPath.String(), "/")
730
731
for idx := len(pathComponents); idx >= 3; idx-- {
732
pretty404 := gopath.Join(append(pathComponents[0:idx], filename404)...)
762
return "", "", fmt.Errorf("there is no 404 file for the requested content types")
763
}
764
765
+// returns unquoted path with all special characters revealed as \u codes
766
+func debugStr(path string) string {
767
+ q := fmt.Sprintf("%+q", path)
768
+ if len(q) >= 3 {
769
+ q = q[1 : len(q)-1]
770
+ }
771
+ return q
772
+}
773
+
774
// Attempt to fix redundant /ipfs/ namespace as long as resulting
775
// 'intended' path is valid. This is in case gremlins were tickled
776
// wrong way and user ended up at /ipfs/ipfs/{cid} or /ipfs/ipns/{id}