From db1b8b290258c869f5a58abad6a416e94cbaf394 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Wed, 25 Jan 2023 11:18:40 +0100 Subject: [PATCH 1/2] refactor: prepare gateway for extraction --- assets/README.md | 10 - assets/assets.go | 36 +--- assets/dag-index-html/README.md | 3 - assets/dag-index-html/index.go | 81 -------- assets/dir-index-html/README.md | 26 --- assets/dir-index-html/index.go | 1 - assets/dir-index-html/package.json | 17 -- assets/dir-index-html/test/go.mod | 3 - assets/dir-index-html/test/main.go | 116 ----------- core/corehttp/gateway.go | 97 +--------- core/corehttp/gateway/README.md | 35 ++++ core/corehttp/gateway/assets/README.md | 27 +++ .../assets/assets.go} | 180 ++++++++++++------ core/corehttp/gateway/assets/build.sh | 14 ++ core/corehttp/gateway/assets/dag-index.html | 67 +++++++ .../gateway/assets/directory-index.html | 0 .../corehttp/gateway/assets}/knownIcons.txt | 0 .../gateway/assets/src/dag-index.html | 66 +++++++ .../gateway/assets/src/directory-index.html | 0 .../corehttp/gateway/assets}/src/icons.css | 0 .../corehttp/gateway/assets}/src/style.css | 0 core/corehttp/gateway/assets/test/main.go | 126 ++++++++++++ core/corehttp/gateway/gateway.go | 107 +++++++++++ .../handler.go} | 67 ++++--- .../handler_block.go} | 7 +- .../handler_car.go} | 7 +- .../handler_codec.go} | 18 +- .../handler_ipns_record.go} | 4 +- .../handler_tar.go} | 7 +- core/corehttp/gateway/handler_test.go | 28 +++ .../handler_unixfs.go} | 7 +- .../handler_unixfs__redirects.go} | 20 +- .../handler_unixfs_dir.go} | 25 ++- .../handler_unixfs_file.go} | 7 +- core/corehttp/{ => gateway}/lazyseek.go | 2 +- core/corehttp/{ => gateway}/lazyseek_test.go | 2 +- core/corehttp/gateway_test.go | 25 --- core/corehttp/hostname.go | 7 +- 38 files changed, 689 insertions(+), 556 deletions(-) delete mode 100644 assets/dag-index-html/README.md delete mode 100644 assets/dag-index-html/index.go delete mode 100644 assets/dir-index-html/README.md delete mode 100644 assets/dir-index-html/index.go delete mode 100644 assets/dir-index-html/package.json delete mode 100644 assets/dir-index-html/test/go.mod delete mode 100644 assets/dir-index-html/test/main.go create mode 100644 core/corehttp/gateway/README.md create mode 100644 core/corehttp/gateway/assets/README.md rename core/corehttp/{gateway_indexPage.go => gateway/assets/assets.go} (50%) create mode 100755 core/corehttp/gateway/assets/build.sh create mode 100644 core/corehttp/gateway/assets/dag-index.html rename assets/dir-index-html/dir-index.html => core/corehttp/gateway/assets/directory-index.html (100%) rename {assets/dir-index-html => core/corehttp/gateway/assets}/knownIcons.txt (100%) create mode 100644 core/corehttp/gateway/assets/src/dag-index.html rename assets/dir-index-html/src/dir-index.html => core/corehttp/gateway/assets/src/directory-index.html (100%) rename {assets/dir-index-html => core/corehttp/gateway/assets}/src/icons.css (100%) rename {assets/dir-index-html => core/corehttp/gateway/assets}/src/style.css (100%) create mode 100644 core/corehttp/gateway/assets/test/main.go create mode 100644 core/corehttp/gateway/gateway.go rename core/corehttp/{gateway_handler.go => gateway/handler.go} (93%) rename core/corehttp/{gateway_handler_block.go => gateway/handler_block.go} (80%) rename core/corehttp/{gateway_handler_car.go => gateway/handler_car.go} (89%) rename core/corehttp/{gateway_handler_codec.go => gateway/handler_codec.go} (88%) rename core/corehttp/{gateway_handler_ipns_record.go => gateway/handler_ipns_record.go} (89%) rename core/corehttp/{gateway_handler_tar.go => gateway/handler_tar.go} (88%) create mode 100644 core/corehttp/gateway/handler_test.go rename core/corehttp/{gateway_handler_unixfs.go => gateway/handler_unixfs.go} (71%) rename core/corehttp/{gateway_handler_unixfs__redirects.go => gateway/handler_unixfs__redirects.go} (87%) rename core/corehttp/{gateway_handler_unixfs_dir.go => gateway/handler_unixfs_dir.go} (87%) rename core/corehttp/{gateway_handler_unixfs_file.go => gateway/handler_unixfs_file.go} (89%) rename core/corehttp/{ => gateway}/lazyseek.go (98%) rename core/corehttp/{ => gateway}/lazyseek_test.go (99%) diff --git a/assets/README.md b/assets/README.md index 02af2f19ca0..b342d080101 100644 --- a/assets/README.md +++ b/assets/README.md @@ -3,13 +3,3 @@ This directory contains the go-ipfs assets: * Getting started documentation (`init-doc`). -* Directory listing HTML template (`dir-index-html`). - -## Re-generating - -Edit the source files and use `go generate` from within the -assets directory: - -``` -go generate . -``` diff --git a/assets/assets.go b/assets/assets.go index 37cbbfad5db..00792d511d3 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -1,30 +1,22 @@ -//go:generate npm run build --prefix ./dir-index-html/ package assets import ( "embed" "fmt" - "io" - "io/fs" gopath "path" - "strconv" "github.com/ipfs/kubo/core" "github.com/ipfs/kubo/core/coreapi" - "github.com/cespare/xxhash" cid "github.com/ipfs/go-cid" "github.com/ipfs/go-libipfs/files" options "github.com/ipfs/interface-go-ipfs-core/options" "github.com/ipfs/interface-go-ipfs-core/path" ) -//go:embed init-doc dir-index-html/dir-index.html dir-index-html/knownIcons.txt +//go:embed init-doc var Asset embed.FS -// AssetHash a non-cryptographic hash of all embedded assets -var AssetHash string - // initDocPaths lists the paths for the docs we want to seed during --init var initDocPaths = []string{ gopath.Join("init-doc", "about"), @@ -36,32 +28,6 @@ var initDocPaths = []string{ gopath.Join("init-doc", "ping"), } -func init() { - sum := xxhash.New() - err := fs.WalkDir(Asset, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - if d.IsDir() { - return nil - } - - file, err := Asset.Open(path) - if err != nil { - return err - } - defer file.Close() - _, err = io.Copy(sum, file) - return err - }) - if err != nil { - panic("error creating asset sum: " + err.Error()) - } - - AssetHash = strconv.FormatUint(sum.Sum64(), 32) -} - // SeedInitDocs adds the list of embedded init documentation to the passed node, pins it and returns the root key func SeedInitDocs(nd *core.IpfsNode) (cid.Cid, error) { return addAssetList(nd, initDocPaths) diff --git a/assets/dag-index-html/README.md b/assets/dag-index-html/README.md deleted file mode 100644 index de38a9504a1..00000000000 --- a/assets/dag-index-html/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# dag-index-html - -> HTML representation for non-UnixFS DAGs such as DAG-CBOR. diff --git a/assets/dag-index-html/index.go b/assets/dag-index-html/index.go deleted file mode 100644 index 214b06a3822..00000000000 --- a/assets/dag-index-html/index.go +++ /dev/null @@ -1,81 +0,0 @@ -package dagindexhtml - -import "html/template" - -// TODO: DagIndexTemplate - replace static CSS with shared one with ../dir-index-html - -// DagIndexTemplate is HTML-based template for non-UnixFS DAGs when request was -// made with Accept: text/html (web browsers). -var DagIndexTemplate = template.Must(template.New("redirect").Parse(` - - - - - - - - - - - - - - - - - - {{ .Path }} - - - - -
-
-

CID: {{.CID}}
- Codec: {{.CodecName}} ({{.CodecHex}})

-
-
- - - - - - - -
-

Preview as JSON
(application/json)

-
-

Or download as: -

-

-
-
-
- -`)) - -type DagIndexTemplateData struct { - Path string - CID string - CodecName string - CodecHex string -} diff --git a/assets/dir-index-html/README.md b/assets/dir-index-html/README.md deleted file mode 100644 index 3dd45eb5905..00000000000 --- a/assets/dir-index-html/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# dir-index-html - -> Directory listing HTML for HTTP gateway - -![](https://user-images.githubusercontent.com/157609/88379209-ce6f0600-cda2-11ea-9620-20b9237bb441.png) - -## Updating - -When making updates to the directory listing page template, please note the following: - -1. Make your changes to the (human-friendly) source documents in the `src` directory and run `npm run build` -3. Before testing or releasing, go to the top-level `./assets` directory and make sure to run the `go generate .` script to update the bindata version - -## Testing - -1. Make sure you have [Go](https://golang.org/dl/) installed -2. Start the test server, which lives in its own directory: - -```bash -> cd test -> go run . -``` -This will listen on [`localhost:3000`](http://localhost:3000/) and reload the template every time you refresh the page. - -If you get a "no such file or directory" error upon trying `go run .`, make sure you ran `npm run build` to generate the minified artifact that the test is looking for. - diff --git a/assets/dir-index-html/index.go b/assets/dir-index-html/index.go deleted file mode 100644 index 98933e3f612..00000000000 --- a/assets/dir-index-html/index.go +++ /dev/null @@ -1 +0,0 @@ -package dirindexhtml diff --git a/assets/dir-index-html/package.json b/assets/dir-index-html/package.json deleted file mode 100644 index 4b242757406..00000000000 --- a/assets/dir-index-html/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "dir-index-html", - "description": "Directory listing HTML for go-ipfs gateways", - "version": "1.3.0", - "private": true, - "homepage": "https://github.com/ipfs/go-ipfs", - "license": "MIT", - "scripts": { - "start": "cd test && go run .", - "build": "npm run build:clean && npm run build:remove-style-links && npm run build:minify-wrap-css && npm run build:combine-html-css && npm run build:remove-unused", - "build:clean": "rm dir-index.html", - "build:remove-style-links": "sed '/ ./base-html.html", - "build:minify-wrap-css": "(echo \"\") > ./minified-wrapped-style.html", - "build:combine-html-css": "sed '/<\\/title>/ r ./minified-wrapped-style.html' ./base-html.html > ./dir-index.html", - "build:remove-unused": "rm ./base-html.html && rm ./minified-wrapped-style.html" - } -} diff --git a/assets/dir-index-html/test/go.mod b/assets/dir-index-html/test/go.mod deleted file mode 100644 index c1cff1b746f..00000000000 --- a/assets/dir-index-html/test/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/ipfs/dir-index-html/test - -go 1.17 diff --git a/assets/dir-index-html/test/main.go b/assets/dir-index-html/test/main.go deleted file mode 100644 index c02523a9f40..00000000000 --- a/assets/dir-index-html/test/main.go +++ /dev/null @@ -1,116 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "net/url" - "os" - "text/template" -) - -const templateFile = "../dir-index.html" - -// Copied from go-ipfs/core/corehttp/gateway_indexPage.go -type listingTemplateData struct { - GatewayURL string - DNSLink bool - Listing []directoryItem - Size string - Path string - Breadcrumbs []breadcrumb - BackLink string - Hash string -} - -type directoryItem struct { - Size string - Name string - Path string - Hash string - ShortHash string -} - -type breadcrumb struct { - Name string - Path string -} - -var testPath = "/ipfs/QmFooBarQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7/a/b/c" -var testData = listingTemplateData{ - GatewayURL: "//localhost:3000", - DNSLink: true, - Listing: []directoryItem{{ - Size: "25 MiB", - Name: "short-film.mov", - Path: testPath + "/short-film.mov", - Hash: "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", - ShortHash: "QmbW\u2026sMnR", - }, { - Size: "23 KiB", - Name: "250pxيوسف_الوزاني_صورة_ملتقطة_بواسطة_مرصد_هابل_الفضائي_توضح_سديم_السرطان،_وهو_بقايا_مستعر_أعظم._.jpg", - Path: testPath + "/250pxيوسف_الوزاني_صورة_ملتقطة_بواسطة_مرصد_هابل_الفضائي_توضح_سديم_السرطان،_وهو_بقايا_مستعر_أعظم._.jpg", - Hash: "QmUwrKrMTrNv8QjWGKMMH5QV9FMPUtRCoQ6zxTdgxATQW6", - ShortHash: "QmUw\u2026TQW6", - }, { - Size: "1 KiB", - Name: "this-piece-of-papers-got-47-words-37-sentences-58-words-we-wanna-know.txt", - Path: testPath + "/this-piece-of-papers-got-47-words-37-sentences-58-words-we-wanna-know.txt", - Hash: "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", - ShortHash: "bafy\u2026bzdi", - }}, - Size: "25 MiB", - Path: testPath, - Breadcrumbs: []breadcrumb{{ - Name: "ipfs", - }, { - Name: "QmFooBarQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7", - Path: testPath + "/../../..", - }, { - Name: "a", - Path: testPath + "/../..", - }, { - Name: "b", - Path: testPath + "/..", - }, { - Name: "c", - Path: testPath, - }}, - BackLink: testPath + "/..", - Hash: "QmFooBazBar2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7", -} - -func main() { - mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - http.Error(w, "Ha-ha, tricked you! There are no files here!", http.StatusNotFound) - return - } - listingTemplate, err := template.New("dir-index.html").Funcs(template.FuncMap{ - "iconFromExt": func(name string) string { - return "ipfs-_blank" // place-holder - }, - "urlEscape": func(rawUrl string) string { - pathUrl := url.URL{Path: rawUrl} - return pathUrl.String() - }, - }).ParseFiles(templateFile) - if err != nil { - http.Error(w, fmt.Sprintf("failed to parse template file: %s", err), http.StatusInternalServerError) - return - } - err = listingTemplate.Execute(w, &testData) - if err != nil { - http.Error(w, fmt.Sprintf("failed to execute template: %s", err), http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusOK) - }) - if _, err := os.Stat(templateFile); err != nil { - wd, _ := os.Getwd() - fmt.Printf("could not open template file %q, relative to %q: %s\n", templateFile, wd, err) - os.Exit(1) - } - fmt.Printf("listening on localhost:3000\n") - http.ListenAndServe("localhost:3000", mux) -} diff --git a/core/corehttp/gateway.go b/core/corehttp/gateway.go index 00c2f748386..7d7674ef0ae 100644 --- a/core/corehttp/gateway.go +++ b/core/corehttp/gateway.go @@ -1,66 +1,19 @@ package corehttp import ( - "context" "fmt" "net" "net/http" - "sort" - coreiface "github.com/ipfs/interface-go-ipfs-core" options "github.com/ipfs/interface-go-ipfs-core/options" - path "github.com/ipfs/interface-go-ipfs-core/path" version "github.com/ipfs/kubo" core "github.com/ipfs/kubo/core" coreapi "github.com/ipfs/kubo/core/coreapi" + "github.com/ipfs/kubo/core/corehttp/gateway" id "github.com/libp2p/go-libp2p/p2p/protocol/identify" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) -type GatewayConfig struct { - Headers map[string][]string - Writable bool -} - -// NodeAPI defines the minimal set of API services required by a gateway handler -type NodeAPI interface { - // Unixfs returns an implementation of Unixfs API - Unixfs() coreiface.UnixfsAPI - - // Block returns an implementation of Block API - Block() coreiface.BlockAPI - - // Dag returns an implementation of Dag API - Dag() coreiface.APIDagService - - // Routing returns an implementation of Routing API. - // Used for returning signed IPNS records, see IPIP-0328 - Routing() coreiface.RoutingAPI - - // ResolvePath resolves the path using Unixfs resolver - ResolvePath(context.Context, path.Path) (path.Resolved, error) -} - -// A helper function to clean up a set of headers: -// 1. Canonicalizes. -// 2. Deduplicates. -// 3. Sorts. -func cleanHeaderSet(headers []string) []string { - // Deduplicate and canonicalize. - m := make(map[string]struct{}, len(headers)) - for _, h := range headers { - m[http.CanonicalHeaderKey(h)] = struct{}{} - } - result := make([]string, 0, len(m)) - for k := range m { - result = append(result, k) - } - - // Sort - sort.Strings(result) - return result -} - func GatewayOption(writable bool, paths ...string) ServeOption { return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) { cfg, err := n.Repo.Config() @@ -78,14 +31,14 @@ func GatewayOption(writable bool, paths ...string) ServeOption { headers[http.CanonicalHeaderKey(h)] = v } - AddAccessControlHeaders(headers) + gateway.AddAccessControlHeaders(headers) offlineAPI, err := api.WithOptions(options.Api.Offline(true)) if err != nil { return nil, err } - gateway := NewGatewayHandler(GatewayConfig{ + gateway := gateway.NewHandler(gateway.Config{ Headers: headers, Writable: writable, }, api, offlineAPI) @@ -99,50 +52,6 @@ func GatewayOption(writable bool, paths ...string) ServeOption { } } -// AddAccessControlHeaders adds default headers used for controlling -// cross-origin requests. This function adds several values to the -// Access-Control-Allow-Headers and Access-Control-Expose-Headers entries. -// If the Access-Control-Allow-Origin entry is missing a value of '*' is -// added, indicating that browsers should allow requesting code from any -// origin to access the resource. -// If the Access-Control-Allow-Methods entry is missing a value of 'GET' is -// added, indicating that browsers may use the GET method when issuing cross -// origin requests. -func AddAccessControlHeaders(headers map[string][]string) { - // Hard-coded headers. - const ACAHeadersName = "Access-Control-Allow-Headers" - const ACEHeadersName = "Access-Control-Expose-Headers" - const ACAOriginName = "Access-Control-Allow-Origin" - const ACAMethodsName = "Access-Control-Allow-Methods" - - if _, ok := headers[ACAOriginName]; !ok { - // Default to *all* - headers[ACAOriginName] = []string{"*"} - } - if _, ok := headers[ACAMethodsName]; !ok { - // Default to GET - headers[ACAMethodsName] = []string{http.MethodGet} - } - - headers[ACAHeadersName] = cleanHeaderSet( - append([]string{ - "Content-Type", - "User-Agent", - "Range", - "X-Requested-With", - }, headers[ACAHeadersName]...)) - - headers[ACEHeadersName] = cleanHeaderSet( - append([]string{ - "Content-Length", - "Content-Range", - "X-Chunked-Output", - "X-Stream-Output", - "X-Ipfs-Path", - "X-Ipfs-Roots", - }, headers[ACEHeadersName]...)) -} - func VersionOption() ServeOption { return func(_ *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) { mux.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { diff --git a/core/corehttp/gateway/README.md b/core/corehttp/gateway/README.md new file mode 100644 index 00000000000..102121d92de --- /dev/null +++ b/core/corehttp/gateway/README.md @@ -0,0 +1,35 @@ +# IPFS Gateway + +> IPFS Gateway HTTP handler. + +## Documentation + +* Go Documentation: https://pkg.go.dev/github.com/ipfs/kubo/core/corehttp/gateway + +## Example + +```go +// Initialize your headers and apply the default headers. +headers := map[string][]string{} +gateway.AddAccessControlHeaders(headers) + +conf := gateway.Config{ + Writable: false, + Headers: headers, +} + +// Initialize a NodeAPI interface for both an online and offline versions. +// The offline version should not make any network request for missing content. +ipfs := ... +offlineIPFS := ... + +// Create http mux and setup gateway handler. +mux := http.NewServeMux() +gwHandler := gateway.NewHandler(conf, ipfs, offlineIPFS) +mux.Handle("/ipfs/", gwHandler) +mux.Handle("/ipns/", gwHandler) + +// Start the server on :8080 and voilá! You have an IPFS gateway running +// in http://localhost:8080. +_ = http.ListenAndServe(":8080", mux) +``` \ No newline at end of file diff --git a/core/corehttp/gateway/assets/README.md b/core/corehttp/gateway/assets/README.md new file mode 100644 index 00000000000..25d1a35e80d --- /dev/null +++ b/core/corehttp/gateway/assets/README.md @@ -0,0 +1,27 @@ +# Required Assets for the Gateway + +> DAG and Directory HTML for HTTP gateway + +## Updating + +When making updates to the templates, please note the following: + +1. Make your changes to the (human-friendly) source documents in the `src` directory. +2. Before testing or releasing, go to `assets/` and run `go generate .`. + +## Testing + +1. Make sure you have [Go](https://golang.org/dl/) installed +2. Start the test server, which lives in its own directory: + +```bash +> cd test +> go run . +``` + +This will listen on [`localhost:3000`](http://localhost:3000/) and reload the template every time you refresh the page. Here you have two pages: + +- [`localhost:3000/dag`](http://localhost:3000/dag) for the DAG template preview; and +- [`localhost:3000/directory`](http://localhost:3000/directory) for the Directory template preview. + +If you get a "no such file or directory" error upon trying `go run .`, make sure you ran `go generate .` to generate the minified artifact that the test is looking for. diff --git a/core/corehttp/gateway_indexPage.go b/core/corehttp/gateway/assets/assets.go similarity index 50% rename from core/corehttp/gateway_indexPage.go rename to core/corehttp/gateway/assets/assets.go index b0db8ac1a18..2e442dd1347 100644 --- a/core/corehttp/gateway_indexPage.go +++ b/core/corehttp/gateway/assets/assets.go @@ -1,28 +1,131 @@ -package corehttp +//go:generate ./build.sh +package assets import ( + "embed" + "io" + "io/fs" + "net" + "strconv" + "html/template" "net/url" "path" "strings" + "github.com/cespare/xxhash" + ipfspath "github.com/ipfs/go-path" - "github.com/ipfs/kubo/assets" ) -// structs for directory listing -type listingTemplateData struct { +//go:embed dag-index.html directory-index.html knownIcons.txt +var asset embed.FS + +// AssetHash a non-cryptographic hash of all embedded assets +var AssetHash string + +var ( + DirectoryTemplate *template.Template + DagTemplate *template.Template +) + +func init() { + initAssetsHash() + initTemplates() +} + +func initAssetsHash() { + sum := xxhash.New() + err := fs.WalkDir(asset, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + file, err := asset.Open(path) + if err != nil { + return err + } + defer file.Close() + _, err = io.Copy(sum, file) + return err + }) + if err != nil { + panic("error creating asset sum: " + err.Error()) + } + + AssetHash = strconv.FormatUint(sum.Sum64(), 32) +} + +func initTemplates() { + knownIconsBytes, err := asset.ReadFile("knownIcons.txt") + if err != nil { + panic(err) + } + knownIcons := make(map[string]struct{}) + for _, ext := range strings.Split(strings.TrimSuffix(string(knownIconsBytes), "\n"), "\n") { + knownIcons[ext] = struct{}{} + } + + // helper to guess the type/icon for it by the extension name + iconFromExt := func(name string) string { + ext := path.Ext(name) + _, ok := knownIcons[ext] + if !ok { + // default blank icon + return "ipfs-_blank" + } + return "ipfs-" + ext[1:] // slice of the first dot + } + + // custom template-escaping function to escape a full path, including '#' and '?' + urlEscape := func(rawUrl string) string { + pathURL := url.URL{Path: rawUrl} + return pathURL.String() + } + + // Directory listing template + dirIndexBytes, err := asset.ReadFile("directory-index.html") + if err != nil { + panic(err) + } + + DirectoryTemplate = template.Must(template.New("dir").Funcs(template.FuncMap{ + "iconFromExt": iconFromExt, + "urlEscape": urlEscape, + }).Parse(string(dirIndexBytes))) + + // DAG Index template + dagIndexBytes, err := asset.ReadFile("dag-index.html") + if err != nil { + panic(err) + } + + DagTemplate = template.Must(template.New("dir").Parse(string(dagIndexBytes))) +} + +type DagTemplateData struct { + Path string + CID string + CodecName string + CodecHex string +} + +type DirectoryTemplateData struct { GatewayURL string DNSLink bool - Listing []directoryItem + Listing []DirectoryItem Size string Path string - Breadcrumbs []breadcrumb + Breadcrumbs []Breadcrumb BackLink string Hash string } -type directoryItem struct { +type DirectoryItem struct { Size string Name string Path string @@ -30,33 +133,33 @@ type directoryItem struct { ShortHash string } -type breadcrumb struct { +type Breadcrumb struct { Name string Path string } -func breadcrumbs(urlPath string, dnslinkOrigin bool) []breadcrumb { - var ret []breadcrumb +func Breadcrumbs(urlPath string, dnslinkOrigin bool) []Breadcrumb { + var ret []Breadcrumb p, err := ipfspath.ParsePath(urlPath) if err != nil { - // No breadcrumbs, fallback to bare Path in template + // No assets.Breadcrumbs, fallback to bare Path in template return ret } segs := p.Segments() contentRoot := segs[1] for i, seg := range segs { if i == 0 { - ret = append(ret, breadcrumb{Name: seg}) + ret = append(ret, Breadcrumb{Name: seg}) } else { - ret = append(ret, breadcrumb{ + ret = append(ret, Breadcrumb{ Name: seg, Path: "/" + strings.Join(segs[0:i+1], "/"), }) } } - // Drop the /ipns/ prefix from breadcrumb Paths when directory + // Drop the /ipns/ prefix from assets.Breadcrumb Paths when directory // listing on a DNSLink website (loaded due to Host header in HTTP // request). Necessary because the hostname most likely won't have a // public gateway mounted. @@ -67,14 +170,14 @@ func breadcrumbs(urlPath string, dnslinkOrigin bool) []breadcrumb { ret[i].Path = strings.Replace(crumb.Path, prefix, "", 1) } } - // Make contentRoot breadcrumb link to the website root + // Make contentRoot assets.Breadcrumb link to the website root ret[1].Path = "/" } return ret } -func shortHash(hash string) string { +func ShortHash(hash string) string { if len(hash) <= 8 { return hash } @@ -83,7 +186,7 @@ func shortHash(hash string) string { // helper to detect DNSLink website context // (when hostname from gwURL is matching /ipns/ in path) -func hasDNSLinkOrigin(gwURL string, path string) bool { +func HasDNSLinkOrigin(gwURL string, path string) bool { if gwURL != "" { fqdn := stripPort(strings.TrimPrefix(gwURL, "//")) return strings.HasPrefix(path, "/ipns/"+fqdn) @@ -91,43 +194,10 @@ func hasDNSLinkOrigin(gwURL string, path string) bool { return false } -var listingTemplate *template.Template - -func init() { - knownIconsBytes, err := assets.Asset.ReadFile("dir-index-html/knownIcons.txt") - if err != nil { - panic(err) - } - knownIcons := make(map[string]struct{}) - for _, ext := range strings.Split(strings.TrimSuffix(string(knownIconsBytes), "\n"), "\n") { - knownIcons[ext] = struct{}{} - } - - // helper to guess the type/icon for it by the extension name - iconFromExt := func(name string) string { - ext := path.Ext(name) - _, ok := knownIcons[ext] - if !ok { - // default blank icon - return "ipfs-_blank" - } - return "ipfs-" + ext[1:] // slice of the first dot - } - - // custom template-escaping function to escape a full path, including '#' and '?' - urlEscape := func(rawUrl string) string { - pathURL := url.URL{Path: rawUrl} - return pathURL.String() - } - - // Directory listing template - dirIndexBytes, err := assets.Asset.ReadFile("dir-index-html/dir-index.html") - if err != nil { - panic(err) +func stripPort(hostname string) string { + host, _, err := net.SplitHostPort(hostname) + if err == nil { + return host } - - listingTemplate = template.Must(template.New("dir").Funcs(template.FuncMap{ - "iconFromExt": iconFromExt, - "urlEscape": urlEscape, - }).Parse(string(dirIndexBytes))) + return hostname } diff --git a/core/corehttp/gateway/assets/build.sh b/core/corehttp/gateway/assets/build.sh new file mode 100755 index 00000000000..531bbfc0244 --- /dev/null +++ b/core/corehttp/gateway/assets/build.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +set -euo pipefail + +function build() { + rm -f $1 + sed '/ ./base-html.html + (echo "") > ./minified-wrapped-style.html + sed '/<\/title>/ r ./minified-wrapped-style.html' ./base-html.html > ./$1 + rm ./base-html.html && rm ./minified-wrapped-style.html +} + +build "directory-index.html" +build "dag-index.html" diff --git a/core/corehttp/gateway/assets/dag-index.html b/core/corehttp/gateway/assets/dag-index.html new file mode 100644 index 00000000000..5bba8f5c0d5 --- /dev/null +++ b/core/corehttp/gateway/assets/dag-index.html @@ -0,0 +1,67 @@ + +{{ $root := . }} + + + + + + + + + + + + + + + + + +{{ .Path }} + + + + +
+
+

CID: {{.CID}}
+ Codec: {{.CodecName}} ({{.CodecHex}})

+
+
+ + + + + + + +
+

Preview as JSON
(application/json)

+
+

Or download as: +

+

+
+
+
+ + diff --git a/assets/dir-index-html/dir-index.html b/core/corehttp/gateway/assets/directory-index.html similarity index 100% rename from assets/dir-index-html/dir-index.html rename to core/corehttp/gateway/assets/directory-index.html diff --git a/assets/dir-index-html/knownIcons.txt b/core/corehttp/gateway/assets/knownIcons.txt similarity index 100% rename from assets/dir-index-html/knownIcons.txt rename to core/corehttp/gateway/assets/knownIcons.txt diff --git a/core/corehttp/gateway/assets/src/dag-index.html b/core/corehttp/gateway/assets/src/dag-index.html new file mode 100644 index 00000000000..7a42ef6bed7 --- /dev/null +++ b/core/corehttp/gateway/assets/src/dag-index.html @@ -0,0 +1,66 @@ + +{{ $root := . }} + + + + + + + + + + + + + + + + + + + +{{ .Path }} + + + +
+
+

CID: {{.CID}}
+ Codec: {{.CodecName}} ({{.CodecHex}})

+
+
+ + + + + + + +
+

Preview as JSON
(application/json)

+
+

Or download as: +

+

+
+
+
+ + diff --git a/assets/dir-index-html/src/dir-index.html b/core/corehttp/gateway/assets/src/directory-index.html similarity index 100% rename from assets/dir-index-html/src/dir-index.html rename to core/corehttp/gateway/assets/src/directory-index.html diff --git a/assets/dir-index-html/src/icons.css b/core/corehttp/gateway/assets/src/icons.css similarity index 100% rename from assets/dir-index-html/src/icons.css rename to core/corehttp/gateway/assets/src/icons.css diff --git a/assets/dir-index-html/src/style.css b/core/corehttp/gateway/assets/src/style.css similarity index 100% rename from assets/dir-index-html/src/style.css rename to core/corehttp/gateway/assets/src/style.css diff --git a/core/corehttp/gateway/assets/test/main.go b/core/corehttp/gateway/assets/test/main.go new file mode 100644 index 00000000000..dc3c8c46472 --- /dev/null +++ b/core/corehttp/gateway/assets/test/main.go @@ -0,0 +1,126 @@ +package main + +import ( + "fmt" + "html/template" + "net/http" + "net/url" + "os" + + "github.com/ipfs/kubo/core/corehttp/gateway/assets" +) + +const ( + directoryTemplateFile = "../directory-index.html" + dagTemplateFile = "../dag-index.html" + + testPath = "/ipfs/QmFooBarQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7/a/b/c" +) + +var directoryTestData = assets.DirectoryTemplateData{ + GatewayURL: "//localhost:3000", + DNSLink: true, + Listing: []assets.DirectoryItem{{ + Size: "25 MiB", + Name: "short-film.mov", + Path: testPath + "/short-film.mov", + Hash: "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", + ShortHash: "QmbW\u2026sMnR", + }, { + Size: "23 KiB", + Name: "250pxيوسف_الوزاني_صورة_ملتقطة_بواسطة_مرصد_هابل_الفضائي_توضح_سديم_السرطان،_وهو_بقايا_مستعر_أعظم._.jpg", + Path: testPath + "/250pxيوسف_الوزاني_صورة_ملتقطة_بواسطة_مرصد_هابل_الفضائي_توضح_سديم_السرطان،_وهو_بقايا_مستعر_أعظم._.jpg", + Hash: "QmUwrKrMTrNv8QjWGKMMH5QV9FMPUtRCoQ6zxTdgxATQW6", + ShortHash: "QmUw\u2026TQW6", + }, { + Size: "1 KiB", + Name: "this-piece-of-papers-got-47-words-37-sentences-58-words-we-wanna-know.txt", + Path: testPath + "/this-piece-of-papers-got-47-words-37-sentences-58-words-we-wanna-know.txt", + Hash: "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ShortHash: "bafy\u2026bzdi", + }}, + Size: "25 MiB", + Path: testPath, + Breadcrumbs: []assets.Breadcrumb{{ + Name: "ipfs", + }, { + Name: "QmFooBarQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7", + Path: testPath + "/../../..", + }, { + Name: "a", + Path: testPath + "/../..", + }, { + Name: "b", + Path: testPath + "/..", + }, { + Name: "c", + Path: testPath, + }}, + BackLink: testPath + "/..", + Hash: "QmFooBazBar2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7", +} + +var dagTestData = assets.DagTemplateData{ + Path: "/ipfs/baguqeerabn4wonmz6icnk7dfckuizcsf4e4igua2ohdboecku225xxmujepa", + CID: "baguqeerabn4wonmz6icnk7dfckuizcsf4e4igua2ohdboecku225xxmujepa", + CodecName: "dag-json", + CodecHex: "0x129", +} + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/dag": + dagTemplate, err := template.New("dag-index.html").ParseFiles(dagTemplateFile) + if err != nil { + http.Error(w, fmt.Sprintf("failed to parse template file: %s", err), http.StatusInternalServerError) + return + } + err = dagTemplate.Execute(w, &dagTestData) + if err != nil { + http.Error(w, fmt.Sprintf("failed to execute template: %s", err), http.StatusInternalServerError) + return + } + case "/directory": + directoryTemplate, err := template.New("directory-index.html").Funcs(template.FuncMap{ + "iconFromExt": func(name string) string { + return "ipfs-_blank" // place-holder + }, + "urlEscape": func(rawUrl string) string { + pathURL := url.URL{Path: rawUrl} + return pathURL.String() + }, + }).ParseFiles(directoryTemplateFile) + if err != nil { + http.Error(w, fmt.Sprintf("failed to parse template file: %s", err), http.StatusInternalServerError) + return + } + err = directoryTemplate.Execute(w, &directoryTestData) + if err != nil { + http.Error(w, fmt.Sprintf("failed to execute template: %s", err), http.StatusInternalServerError) + return + } + case "/": + html := `

Test paths: DAG, Directory.` + _, _ = w.Write([]byte(html)) + default: + http.Redirect(w, r, "/", http.StatusSeeOther) + } + }) + + if _, err := os.Stat(directoryTemplateFile); err != nil { + wd, _ := os.Getwd() + fmt.Printf("could not open template file %q, relative to %q: %s\n", directoryTemplateFile, wd, err) + os.Exit(1) + } + + if _, err := os.Stat(dagTemplateFile); err != nil { + wd, _ := os.Getwd() + fmt.Printf("could not open template file %q, relative to %q: %s\n", dagTemplateFile, wd, err) + os.Exit(1) + } + + fmt.Printf("listening on localhost:3000\n") + _ = http.ListenAndServe("localhost:3000", mux) +} diff --git a/core/corehttp/gateway/gateway.go b/core/corehttp/gateway/gateway.go new file mode 100644 index 00000000000..0882d4fb438 --- /dev/null +++ b/core/corehttp/gateway/gateway.go @@ -0,0 +1,107 @@ +package gateway + +import ( + "context" + "net/http" + "sort" + + coreiface "github.com/ipfs/interface-go-ipfs-core" + path "github.com/ipfs/interface-go-ipfs-core/path" +) + +// Config is the configuration that will be applied when creating a new gateway +// handler. +type Config struct { + Headers map[string][]string + Writable bool +} + +// NodeAPI defines the minimal set of API services required by a gateway handler +type NodeAPI interface { + // Unixfs returns an implementation of Unixfs API + Unixfs() coreiface.UnixfsAPI + + // Block returns an implementation of Block API + Block() coreiface.BlockAPI + + // Dag returns an implementation of Dag API + Dag() coreiface.APIDagService + + // Routing returns an implementation of Routing API. + // Used for returning signed IPNS records, see IPIP-0328 + Routing() coreiface.RoutingAPI + + // ResolvePath resolves the path using Unixfs resolver + ResolvePath(context.Context, path.Path) (path.Resolved, error) +} + +// A helper function to clean up a set of headers: +// 1. Canonicalizes. +// 2. Deduplicates. +// 3. Sorts. +func cleanHeaderSet(headers []string) []string { + // Deduplicate and canonicalize. + m := make(map[string]struct{}, len(headers)) + for _, h := range headers { + m[http.CanonicalHeaderKey(h)] = struct{}{} + } + result := make([]string, 0, len(m)) + for k := range m { + result = append(result, k) + } + + // Sort + sort.Strings(result) + return result +} + +// AddAccessControlHeaders adds default headers used for controlling +// cross-origin requests. This function adds several values to the +// Access-Control-Allow-Headers and Access-Control-Expose-Headers entries. +// If the Access-Control-Allow-Origin entry is missing a value of '*' is +// added, indicating that browsers should allow requesting code from any +// origin to access the resource. +// If the Access-Control-Allow-Methods entry is missing a value of 'GET' is +// added, indicating that browsers may use the GET method when issuing cross +// origin requests. +func AddAccessControlHeaders(headers map[string][]string) { + // Hard-coded headers. + const ACAHeadersName = "Access-Control-Allow-Headers" + const ACEHeadersName = "Access-Control-Expose-Headers" + const ACAOriginName = "Access-Control-Allow-Origin" + const ACAMethodsName = "Access-Control-Allow-Methods" + + if _, ok := headers[ACAOriginName]; !ok { + // Default to *all* + headers[ACAOriginName] = []string{"*"} + } + if _, ok := headers[ACAMethodsName]; !ok { + // Default to GET + headers[ACAMethodsName] = []string{http.MethodGet} + } + + headers[ACAHeadersName] = cleanHeaderSet( + append([]string{ + "Content-Type", + "User-Agent", + "Range", + "X-Requested-With", + }, headers[ACAHeadersName]...)) + + headers[ACEHeadersName] = cleanHeaderSet( + append([]string{ + "Content-Length", + "Content-Range", + "X-Chunked-Output", + "X-Stream-Output", + "X-Ipfs-Path", + "X-Ipfs-Roots", + }, headers[ACEHeadersName]...)) +} + +type RequestContextKey string + +const ( + DNSLinkHostnameKey RequestContextKey = "dnslink-hostname" + GatewayHostnameKey RequestContextKey = "gw-hostname" +) diff --git a/core/corehttp/gateway_handler.go b/core/corehttp/gateway/handler.go similarity index 93% rename from core/corehttp/gateway_handler.go rename to core/corehttp/gateway/handler.go index c3e8fa0d63d..e6354069a6c 100644 --- a/core/corehttp/gateway_handler.go +++ b/core/corehttp/gateway/handler.go @@ -1,4 +1,4 @@ -package corehttp +package gateway import ( "context" @@ -19,6 +19,7 @@ import ( cid "github.com/ipfs/go-cid" ipld "github.com/ipfs/go-ipld-format" "github.com/ipfs/go-libipfs/files" + logging "github.com/ipfs/go-log" dag "github.com/ipfs/go-merkledag" mfs "github.com/ipfs/go-mfs" path "github.com/ipfs/go-path" @@ -28,11 +29,14 @@ import ( 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" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) +var log = logging.Logger("core/server") + const ( ipfsPathPrefix = "/ipfs/" ipnsPathPrefix = "/ipns/" @@ -64,10 +68,10 @@ type redirectTemplateData struct { ErrorMsg string } -// gatewayHandler is a HTTP handler that serves IPFS objects (accessible by default at /ipfs/) +// handler is a HTTP handler that serves IPFS objects (accessible by default at /ipfs/) // (it serves requests like GET /ipfs/QmVRzPKPzNtSrEzBFm2UZfxmPAgnaLke4DMcerbsGGSaFe/link) -type gatewayHandler struct { - config GatewayConfig +type handler struct { + config Config api NodeAPI offlineAPI NodeAPI @@ -169,7 +173,7 @@ func (w *errRecordingResponseWriter) ReadFrom(r io.Reader) (n int64, err error) return n, err } -func newGatewaySummaryMetric(name string, help string) *prometheus.SummaryVec { +func newSummaryMetric(name string, help string) *prometheus.SummaryVec { summaryMetric := prometheus.NewSummaryVec( prometheus.SummaryOpts{ Namespace: "ipfs", @@ -189,7 +193,7 @@ func newGatewaySummaryMetric(name string, help string) *prometheus.SummaryVec { return summaryMetric } -func newGatewayHistogramMetric(name string, help string) *prometheus.HistogramVec { +func newHistogramMetric(name string, help string) *prometheus.HistogramVec { // We can add buckets as a parameter in the future, but for now using static defaults // suggested in https://github.com/ipfs/kubo/issues/8441 defaultBuckets := []float64{0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 30, 60} @@ -213,14 +217,14 @@ func newGatewayHistogramMetric(name string, help string) *prometheus.HistogramVe return histogramMetric } -// NewGatewayHandler returns an http.Handler that can act as a gateway to IPFS content +// NewHandler returns an http.Handler that can act as a gateway to IPFS content // offlineApi is a version of the API that should not make network requests for missing data -func NewGatewayHandler(c GatewayConfig, api NodeAPI, offlineAPI NodeAPI) http.Handler { - return newGatewayHandler(c, api, offlineAPI) +func NewHandler(c Config, api NodeAPI, offlineAPI NodeAPI) http.Handler { + return newHandler(c, api, offlineAPI) } -func newGatewayHandler(c GatewayConfig, api NodeAPI, offlineAPI NodeAPI) *gatewayHandler { - i := &gatewayHandler{ +func newHandler(c Config, api NodeAPI, offlineAPI NodeAPI) *handler { + i := &handler{ config: c, api: api, offlineAPI: offlineAPI, @@ -228,7 +232,7 @@ func newGatewayHandler(c GatewayConfig, api NodeAPI, offlineAPI NodeAPI) *gatewa // ---------------------------- // Time till the first content block (bar in /ipfs/cid/foo/bar) // (format-agnostic, across all response types) - firstContentBlockGetMetric: newGatewayHistogramMetric( + firstContentBlockGetMetric: newHistogramMetric( "gw_first_content_block_get_latency_seconds", "The time till the first content block is received on GET from the gateway.", ), @@ -236,29 +240,29 @@ func newGatewayHandler(c GatewayConfig, api NodeAPI, offlineAPI NodeAPI) *gatewa // Response-type specific metrics // ---------------------------- // UnixFS: time it takes to return a file - unixfsFileGetMetric: newGatewayHistogramMetric( + unixfsFileGetMetric: newHistogramMetric( "gw_unixfs_file_get_duration_seconds", "The time to serve an entire UnixFS file from the gateway.", ), // UnixFS: time it takes to generate static HTML with directory listing - unixfsGenDirGetMetric: newGatewayHistogramMetric( + unixfsGenDirGetMetric: newHistogramMetric( "gw_unixfs_gen_dir_listing_get_duration_seconds", "The time to serve a generated UnixFS HTML directory listing from the gateway.", ), // CAR: time it takes to return requested CAR stream - carStreamGetMetric: newGatewayHistogramMetric( + carStreamGetMetric: newHistogramMetric( "gw_car_stream_get_duration_seconds", "The time to GET an entire CAR stream from the gateway.", ), // Block: time it takes to return requested Block - rawBlockGetMetric: newGatewayHistogramMetric( + rawBlockGetMetric: newHistogramMetric( "gw_raw_block_get_duration_seconds", "The time to GET an entire raw Block from the gateway.", ), // Legacy Metrics // ---------------------------- - unixfsGetMetric: newGatewaySummaryMetric( // TODO: remove? + unixfsGetMetric: newSummaryMetric( // TODO: remove? // (deprecated, use firstContentBlockGetMetric instead) "unixfs_get_latency_seconds", "The time to receive the first UnixFS node on a GET from the gateway.", @@ -287,7 +291,7 @@ func parseIpfsPath(p string) (cid.Cid, string, error) { return rootCid, path.Join(rsegs[2:]), nil } -func (i *gatewayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (i *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // the hour is a hard fallback, we don't expect it to happen, but just in case ctx, cancel := context.WithTimeout(r.Context(), time.Hour) defer cancel() @@ -339,7 +343,7 @@ func (i *gatewayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, errmsg, status) } -func (i *gatewayHandler) optionsHandler(w http.ResponseWriter, r *http.Request) { +func (i *handler) optionsHandler(w http.ResponseWriter, r *http.Request) { /* OPTIONS is a noop request that is used by the browsers to check if server accepts cross-site XMLHttpRequest (indicated by the presence of CORS headers) @@ -348,7 +352,7 @@ func (i *gatewayHandler) optionsHandler(w http.ResponseWriter, r *http.Request) i.addUserHeaders(w) // return all custom headers (including CORS ones, if set) } -func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { +func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { begin := time.Now() logger := log.With("from", r.RequestURI) @@ -455,7 +459,7 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request } } -func (i *gatewayHandler) postHandler(w http.ResponseWriter, r *http.Request) { +func (i *handler) postHandler(w http.ResponseWriter, r *http.Request) { p, err := i.api.Unixfs().Add(r.Context(), files.NewReaderFile(r.Body)) if err != nil { internalWebError(w, err) @@ -468,7 +472,7 @@ func (i *gatewayHandler) postHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, p.String(), http.StatusCreated) } -func (i *gatewayHandler) putHandler(w http.ResponseWriter, r *http.Request) { +func (i *handler) putHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() ds := i.api.Dag() @@ -563,7 +567,7 @@ func (i *gatewayHandler) putHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, redirectURL, http.StatusCreated) } -func (i *gatewayHandler) deleteHandler(w http.ResponseWriter, r *http.Request) { +func (i *handler) deleteHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // parse the path @@ -639,7 +643,7 @@ func (i *gatewayHandler) deleteHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, redirectURL, http.StatusCreated) } -func (i *gatewayHandler) addUserHeaders(w http.ResponseWriter) { +func (i *handler) addUserHeaders(w http.ResponseWriter) { for k, v := range i.config.Headers { w.Header()[k] = v } @@ -706,7 +710,7 @@ func setContentDispositionHeader(w http.ResponseWriter, filename string, disposi } // Set X-Ipfs-Roots with logical CID array for efficient HTTP cache invalidation. -func (i *gatewayHandler) buildIpfsRootsHeader(contentPath string, r *http.Request) (string, error) { +func (i *handler) buildIpfsRootsHeader(contentPath string, r *http.Request) (string, error) { /* These are logical roots where each CID represent one path segment and resolves to either a directory or the root block of a file. @@ -930,7 +934,7 @@ func debugStr(path string) string { // Resolve the provided contentPath including any special handling related to // the requested responseFormat. Returned ok flag indicates if gateway handler // should continue processing the request. -func (i *gatewayHandler) handlePathResolution(w http.ResponseWriter, r *http.Request, responseFormat string, contentPath ipath.Path, logger *zap.SugaredLogger) (resolvedPath ipath.Resolved, newContentPath ipath.Path, ok bool) { +func (i *handler) handlePathResolution(w http.ResponseWriter, r *http.Request, responseFormat string, contentPath ipath.Path, logger *zap.SugaredLogger) (resolvedPath ipath.Resolved, newContentPath ipath.Path, ok bool) { // Attempt to resolve the provided path. resolvedPath, err := i.api.ResolvePath(r.Context(), contentPath) @@ -972,7 +976,7 @@ func (i *gatewayHandler) handlePathResolution(w http.ResponseWriter, r *http.Req // Detect 'Cache-Control: only-if-cached' in request and return data if it is already in the local datastore. // https://github.com/ipfs/specs/blob/main/http-gateways/PATH_GATEWAY.md#cache-control-request-header -func (i *gatewayHandler) handleOnlyIfCached(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, logger *zap.SugaredLogger) (requestHandled bool) { +func (i *handler) handleOnlyIfCached(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, logger *zap.SugaredLogger) (requestHandled bool) { if r.Header.Get("Cache-Control") == "only-if-cached" { _, err := i.offlineAPI.Block().Stat(r.Context(), contentPath) if err != nil { @@ -1089,7 +1093,7 @@ func handleSuperfluousNamespace(w http.ResponseWriter, r *http.Request, contentP return true } -func (i *gatewayHandler) handleGettingFirstBlock(r *http.Request, begin time.Time, contentPath ipath.Path, resolvedPath ipath.Resolved) *requestError { +func (i *handler) handleGettingFirstBlock(r *http.Request, begin time.Time, contentPath ipath.Path, resolvedPath ipath.Resolved) *requestError { // Update the global metric of the time it takes to read the final root block of the requested resource // NOTE: for legacy reasons this happens before we go into content-type specific code paths _, err := i.api.Block().Get(r.Context(), resolvedPath) @@ -1103,7 +1107,7 @@ func (i *gatewayHandler) handleGettingFirstBlock(r *http.Request, begin time.Tim return nil } -func (i *gatewayHandler) setCommonHeaders(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) *requestError { +func (i *handler) setCommonHeaders(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) *requestError { i.addUserHeaders(w) // ok, _now_ write user's headers. w.Header().Set("X-Ipfs-Path", contentPath.String()) @@ -1115,3 +1119,8 @@ func (i *gatewayHandler) setCommonHeaders(w http.ResponseWriter, r *http.Request return nil } + +// spanTrace starts a new span using the standard IPFS tracing conventions. +func spanTrace(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + return otel.Tracer("go-libipfs").Start(ctx, fmt.Sprintf("%s.%s", " Gateway", spanName), opts...) +} diff --git a/core/corehttp/gateway_handler_block.go b/core/corehttp/gateway/handler_block.go similarity index 80% rename from core/corehttp/gateway_handler_block.go rename to core/corehttp/gateway/handler_block.go index 3bf7c76be2d..23a22f44778 100644 --- a/core/corehttp/gateway_handler_block.go +++ b/core/corehttp/gateway/handler_block.go @@ -1,4 +1,4 @@ -package corehttp +package gateway import ( "bytes" @@ -8,14 +8,13 @@ import ( "time" ipath "github.com/ipfs/interface-go-ipfs-core/path" - "github.com/ipfs/kubo/tracing" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) // serveRawBlock returns bytes behind a raw block -func (i *gatewayHandler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time) { - ctx, span := tracing.Span(ctx, "Gateway", "ServeRawBlock", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) +func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time) { + ctx, span := spanTrace(ctx, "ServeRawBlock", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) defer span.End() blockCid := resolvedPath.Cid() blockReader, err := i.api.Block().Get(ctx, resolvedPath) diff --git a/core/corehttp/gateway_handler_car.go b/core/corehttp/gateway/handler_car.go similarity index 89% rename from core/corehttp/gateway_handler_car.go rename to core/corehttp/gateway/handler_car.go index 9f704d6ca95..f58bccfd7ae 100644 --- a/core/corehttp/gateway_handler_car.go +++ b/core/corehttp/gateway/handler_car.go @@ -1,4 +1,4 @@ -package corehttp +package gateway import ( "context" @@ -10,7 +10,6 @@ import ( blocks "github.com/ipfs/go-libipfs/blocks" coreiface "github.com/ipfs/interface-go-ipfs-core" ipath "github.com/ipfs/interface-go-ipfs-core/path" - "github.com/ipfs/kubo/tracing" gocar "github.com/ipld/go-car" selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse" "go.opentelemetry.io/otel/attribute" @@ -18,8 +17,8 @@ import ( ) // serveCAR returns a CAR stream for specific DAG+selector -func (i *gatewayHandler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, carVersion string, begin time.Time) { - ctx, span := tracing.Span(ctx, "Gateway", "ServeCAR", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) +func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, carVersion string, begin time.Time) { + ctx, span := spanTrace(ctx, "ServeCAR", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) defer span.End() ctx, cancel := context.WithCancel(ctx) defer cancel() diff --git a/core/corehttp/gateway_handler_codec.go b/core/corehttp/gateway/handler_codec.go similarity index 88% rename from core/corehttp/gateway_handler_codec.go rename to core/corehttp/gateway/handler_codec.go index 93e9593b7b3..ac219f16545 100644 --- a/core/corehttp/gateway_handler_codec.go +++ b/core/corehttp/gateway/handler_codec.go @@ -1,4 +1,4 @@ -package corehttp +package gateway import ( "bytes" @@ -13,9 +13,7 @@ import ( 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/ipfs/kubo/core/corehttp/gateway/assets" "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/multicodec" mc "github.com/multiformats/go-multicodec" @@ -55,8 +53,8 @@ var contentTypeToExtension = map[string]string{ "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))) +func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, requestedContentType string) { + ctx, span := spanTrace(ctx, "ServeCodec", trace.WithAttributes(attribute.String("path", resolvedPath.String()), attribute.String("requestedContentType", requestedContentType))) defer span.End() cidCodec := mc.Code(resolvedPath.Cid().Prefix().Codec) @@ -134,7 +132,7 @@ func (i *gatewayHandler) serveCodec(ctx context.Context, w http.ResponseWriter, 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) { +func (i *handler) 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") @@ -152,7 +150,7 @@ func (i *gatewayHandler) serveCodecHTML(ctx context.Context, w http.ResponseWrit w.Header().Del("Cache-Control") cidCodec := mc.Code(resolvedPath.Cid().Prefix().Codec) - if err := dih.DagIndexTemplate.Execute(w, dih.DagIndexTemplateData{ + if err := assets.DagTemplate.Execute(w, assets.DagTemplateData{ Path: contentPath.String(), CID: resolvedPath.Cid().String(), CodecName: cidCodec.String(), @@ -163,7 +161,7 @@ func (i *gatewayHandler) serveCodecHTML(ctx context.Context, w http.ResponseWrit } // serveCodecRaw returns the raw block without any conversion -func (i *gatewayHandler) serveCodecRaw(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, name string, modtime time.Time) { +func (i *handler) 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 { @@ -183,7 +181,7 @@ func (i *gatewayHandler) serveCodecRaw(ctx context.Context, w http.ResponseWrite } // serveCodecConverted returns payload converted to codec specified in toCodec -func (i *gatewayHandler) serveCodecConverted(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, toCodec mc.Code, modtime time.Time) { +func (i *handler) serveCodecConverted(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, toCodec mc.Code, modtime time.Time) { obj, err := i.api.Dag().Get(ctx, resolvedPath.Cid()) if err != nil { webError(w, "ipfs dag get "+html.EscapeString(resolvedPath.String()), err, http.StatusInternalServerError) diff --git a/core/corehttp/gateway_handler_ipns_record.go b/core/corehttp/gateway/handler_ipns_record.go similarity index 89% rename from core/corehttp/gateway_handler_ipns_record.go rename to core/corehttp/gateway/handler_ipns_record.go index 16d9663fada..47786c5b7fd 100644 --- a/core/corehttp/gateway_handler_ipns_record.go +++ b/core/corehttp/gateway/handler_ipns_record.go @@ -1,4 +1,4 @@ -package corehttp +package gateway import ( "context" @@ -15,7 +15,7 @@ import ( "go.uber.org/zap" ) -func (i *gatewayHandler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) { +func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) { if contentPath.Namespace() != "ipns" { err := fmt.Errorf("%s is not an IPNS link", contentPath.String()) webError(w, err.Error(), err, http.StatusBadRequest) diff --git a/core/corehttp/gateway_handler_tar.go b/core/corehttp/gateway/handler_tar.go similarity index 88% rename from core/corehttp/gateway_handler_tar.go rename to core/corehttp/gateway/handler_tar.go index 14edf4fbf5f..f5a7a67137f 100644 --- a/core/corehttp/gateway_handler_tar.go +++ b/core/corehttp/gateway/handler_tar.go @@ -1,4 +1,4 @@ -package corehttp +package gateway import ( "context" @@ -8,7 +8,6 @@ import ( "github.com/ipfs/go-libipfs/files" ipath "github.com/ipfs/interface-go-ipfs-core/path" - "github.com/ipfs/kubo/tracing" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" @@ -16,8 +15,8 @@ import ( var unixEpochTime = time.Unix(0, 0) -func (i *gatewayHandler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) { - ctx, span := tracing.Span(ctx, "Gateway", "ServeTAR", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) +func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) { + ctx, span := spanTrace(ctx, "ServeTAR", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) defer span.End() ctx, cancel := context.WithCancel(ctx) diff --git a/core/corehttp/gateway/handler_test.go b/core/corehttp/gateway/handler_test.go new file mode 100644 index 00000000000..d08dc295305 --- /dev/null +++ b/core/corehttp/gateway/handler_test.go @@ -0,0 +1,28 @@ +package gateway + +import "testing" + +func TestEtagMatch(t *testing.T) { + for _, test := range []struct { + header string // value in If-None-Match HTTP header + cidEtag string + dirEtag string + expected bool // expected result of etagMatch(header, cidEtag, dirEtag) + }{ + {"", `"etag"`, "", false}, // no If-None-Match + {"", "", `"etag"`, false}, // no If-None-Match + {`"etag"`, `"etag"`, "", true}, // file etag match + {`W/"etag"`, `"etag"`, "", true}, // file etag match + {`"foo", W/"bar", W/"etag"`, `"etag"`, "", true}, // file etag match (array) + {`"foo",W/"bar",W/"etag"`, `"etag"`, "", true}, // file etag match (compact array) + {`"etag"`, "", `W/"etag"`, true}, // dir etag match + {`"etag"`, "", `W/"etag"`, true}, // dir etag match + {`W/"etag"`, "", `W/"etag"`, true}, // dir etag match + {`*`, `"etag"`, "", true}, // wildcard etag match + } { + result := etagMatch(test.header, test.cidEtag, test.dirEtag) + if result != test.expected { + t.Fatalf("unexpected result of etagMatch(%q, %q, %q), got %t, expected %t", test.header, test.cidEtag, test.dirEtag, result, test.expected) + } + } +} diff --git a/core/corehttp/gateway_handler_unixfs.go b/core/corehttp/gateway/handler_unixfs.go similarity index 71% rename from core/corehttp/gateway_handler_unixfs.go rename to core/corehttp/gateway/handler_unixfs.go index 045c0f81d22..9962d468c90 100644 --- a/core/corehttp/gateway_handler_unixfs.go +++ b/core/corehttp/gateway/handler_unixfs.go @@ -1,4 +1,4 @@ -package corehttp +package gateway import ( "context" @@ -9,14 +9,13 @@ import ( "github.com/ipfs/go-libipfs/files" ipath "github.com/ipfs/interface-go-ipfs-core/path" - "github.com/ipfs/kubo/tracing" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) -func (i *gatewayHandler) serveUnixFS(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) { - ctx, span := tracing.Span(ctx, "Gateway", "ServeUnixFS", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) +func (i *handler) serveUnixFS(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) { + ctx, span := spanTrace(ctx, "ServeUnixFS", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) defer span.End() // Handling UnixFS diff --git a/core/corehttp/gateway_handler_unixfs__redirects.go b/core/corehttp/gateway/handler_unixfs__redirects.go similarity index 87% rename from core/corehttp/gateway_handler_unixfs__redirects.go rename to core/corehttp/gateway/handler_unixfs__redirects.go index 6906683a639..98715cb2a56 100644 --- a/core/corehttp/gateway_handler_unixfs__redirects.go +++ b/core/corehttp/gateway/handler_unixfs__redirects.go @@ -1,4 +1,4 @@ -package corehttp +package gateway import ( "fmt" @@ -36,7 +36,7 @@ import ( // // Note that for security reasons, redirect rules are only processed when the request has origin isolation. // See https://github.com/ipfs/specs/pull/290 for more information. -func (i *gatewayHandler) serveRedirectsIfPresent(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, logger *zap.SugaredLogger) (newResolvedPath ipath.Resolved, newContentPath ipath.Path, continueProcessing bool, hadMatchingRule bool) { +func (i *handler) serveRedirectsIfPresent(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, logger *zap.SugaredLogger) (newResolvedPath ipath.Resolved, newContentPath ipath.Path, continueProcessing bool, hadMatchingRule bool) { redirectsFile := i.getRedirectsFile(r, contentPath, logger) if redirectsFile != nil { redirectRules, err := i.getRedirectRules(r, redirectsFile) @@ -73,7 +73,7 @@ func (i *gatewayHandler) serveRedirectsIfPresent(w http.ResponseWriter, r *http. return resolvedPath, contentPath, true, false } -func (i *gatewayHandler) handleRedirectsFileRules(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, redirectRules []redirects.Rule) (redirected bool, newContentPath string, err error) { +func (i *handler) handleRedirectsFileRules(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, redirectRules []redirects.Rule) (redirected bool, newContentPath string, err error) { // Attempt to match a rule to the URL path, and perform the corresponding redirect or rewrite pathParts := strings.Split(contentPath.String(), "/") if len(pathParts) > 3 { @@ -118,7 +118,7 @@ func (i *gatewayHandler) handleRedirectsFileRules(w http.ResponseWriter, r *http return false, "", nil } -func (i *gatewayHandler) getRedirectRules(r *http.Request, redirectsFilePath ipath.Resolved) ([]redirects.Rule, error) { +func (i *handler) getRedirectRules(r *http.Request, redirectsFilePath ipath.Resolved) ([]redirects.Rule, error) { // Convert the path into a file node node, err := i.api.Unixfs().Get(r.Context(), redirectsFilePath) if err != nil { @@ -142,7 +142,7 @@ func (i *gatewayHandler) getRedirectRules(r *http.Request, redirectsFilePath ipa } // Returns a resolved path to the _redirects file located in the root CID path of the requested path -func (i *gatewayHandler) getRedirectsFile(r *http.Request, contentPath ipath.Path, logger *zap.SugaredLogger) ipath.Resolved { +func (i *handler) getRedirectsFile(r *http.Request, contentPath ipath.Path, logger *zap.SugaredLogger) ipath.Resolved { // contentPath is the full ipfs path to the requested resource, // regardless of whether path or subdomain resolution is used. rootPath := getRootPath(contentPath) @@ -164,7 +164,7 @@ func getRootPath(path ipath.Path) ipath.Path { return ipath.New(gopath.Join("/", path.Namespace(), parts[2])) } -func (i *gatewayHandler) serve4xx(w http.ResponseWriter, r *http.Request, content4xxPath ipath.Path, status int) error { +func (i *handler) serve4xx(w http.ResponseWriter, r *http.Request, content4xxPath ipath.Path, status int) error { resolved4xxPath, err := i.api.ResolvePath(r.Context(), content4xxPath) if err != nil { return err @@ -196,8 +196,8 @@ func (i *gatewayHandler) serve4xx(w http.ResponseWriter, r *http.Request, conten } func hasOriginIsolation(r *http.Request) bool { - _, gw := r.Context().Value(requestContextKey("gw-hostname")).(string) - _, dnslink := r.Context().Value("dnslink-hostname").(string) + _, gw := r.Context().Value(GatewayHostnameKey).(string) + _, dnslink := r.Context().Value(DNSLinkHostnameKey).(string) if gw || dnslink { return true @@ -214,7 +214,7 @@ func isUnixfsResponseFormat(responseFormat string) bool { // Deprecated: legacy ipfs-404.html files are superseded by _redirects file // This is provided only for backward-compatibility, until websites migrate // to 404s managed via _redirects file (https://github.com/ipfs/specs/pull/290) -func (i *gatewayHandler) serveLegacy404IfPresent(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool { +func (i *handler) serveLegacy404IfPresent(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool { resolved404Path, ctype, err := i.searchUpTreeFor404(r, contentPath) if err != nil { return false @@ -244,7 +244,7 @@ func (i *gatewayHandler) serveLegacy404IfPresent(w http.ResponseWriter, r *http. return err == nil } -func (i *gatewayHandler) searchUpTreeFor404(r *http.Request, contentPath ipath.Path) (ipath.Resolved, string, error) { +func (i *handler) searchUpTreeFor404(r *http.Request, contentPath ipath.Path) (ipath.Resolved, string, error) { filename404, ctype, err := preferred404Filename(r.Header.Values("Accept")) if err != nil { return nil, "", err diff --git a/core/corehttp/gateway_handler_unixfs_dir.go b/core/corehttp/gateway/handler_unixfs_dir.go similarity index 87% rename from core/corehttp/gateway_handler_unixfs_dir.go rename to core/corehttp/gateway/handler_unixfs_dir.go index 03d67e1c040..8a66d4ea9ed 100644 --- a/core/corehttp/gateway_handler_unixfs_dir.go +++ b/core/corehttp/gateway/handler_unixfs_dir.go @@ -1,4 +1,4 @@ -package corehttp +package gateway import ( "context" @@ -15,8 +15,7 @@ import ( "github.com/ipfs/go-path/resolver" options "github.com/ipfs/interface-go-ipfs-core/options" ipath "github.com/ipfs/interface-go-ipfs-core/path" - "github.com/ipfs/kubo/assets" - "github.com/ipfs/kubo/tracing" + "github.com/ipfs/kubo/core/corehttp/gateway/assets" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" @@ -25,8 +24,8 @@ import ( // serveDirectory returns the best representation of UnixFS directory // // It will return index.html if present, or generate directory listing otherwise. -func (i *gatewayHandler) serveDirectory(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, dir files.Directory, begin time.Time, logger *zap.SugaredLogger) { - ctx, span := tracing.Span(ctx, "Gateway", "ServeDirectory", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) +func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, dir files.Directory, begin time.Time, logger *zap.SugaredLogger) { + ctx, span := spanTrace(ctx, "ServeDirectory", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) defer span.End() // HostnameOption might have constructed an IPNS/IPFS path using the Host header. @@ -118,7 +117,7 @@ func (i *gatewayHandler) serveDirectory(ctx context.Context, w http.ResponseWrit return } - dirListing := make([]directoryItem, 0, len(results)) + dirListing := make([]assets.DirectoryItem, 0, len(results)) for link := range results { if link.Err != nil { internalWebError(w, err) @@ -126,12 +125,12 @@ func (i *gatewayHandler) serveDirectory(ctx context.Context, w http.ResponseWrit } hash := link.Cid.String() - di := directoryItem{ + di := assets.DirectoryItem{ Size: humanize.Bytes(uint64(link.Size)), Name: link.Name, Path: gopath.Join(originalURLPath, link.Name), Hash: hash, - ShortHash: shortHash(hash), + ShortHash: assets.ShortHash(hash), } dirListing = append(dirListing, di) } @@ -174,29 +173,29 @@ func (i *gatewayHandler) serveDirectory(ctx context.Context, w http.ResponseWrit var gwURL string // Get gateway hostname and build gateway URL. - if h, ok := r.Context().Value(requestContextKey("gw-hostname")).(string); ok { + if h, ok := r.Context().Value(GatewayHostnameKey).(string); ok { gwURL = "//" + h } else { gwURL = "" } - dnslink := hasDNSLinkOrigin(gwURL, contentPath.String()) + dnslink := assets.HasDNSLinkOrigin(gwURL, contentPath.String()) // See comment above where originalUrlPath is declared. - tplData := listingTemplateData{ + tplData := assets.DirectoryTemplateData{ GatewayURL: gwURL, DNSLink: dnslink, Listing: dirListing, Size: size, Path: contentPath.String(), - Breadcrumbs: breadcrumbs(contentPath.String(), dnslink), + Breadcrumbs: assets.Breadcrumbs(contentPath.String(), dnslink), BackLink: backLink, Hash: hash, } logger.Debugw("request processed", "tplDataDNSLink", dnslink, "tplDataSize", size, "tplDataBackLink", backLink, "tplDataHash", hash) - if err := listingTemplate.Execute(w, tplData); err != nil { + if err := assets.DirectoryTemplate.Execute(w, tplData); err != nil { internalWebError(w, err) return } diff --git a/core/corehttp/gateway_handler_unixfs_file.go b/core/corehttp/gateway/handler_unixfs_file.go similarity index 89% rename from core/corehttp/gateway_handler_unixfs_file.go rename to core/corehttp/gateway/handler_unixfs_file.go index 1abdc823e73..a4f7d4cd9e2 100644 --- a/core/corehttp/gateway_handler_unixfs_file.go +++ b/core/corehttp/gateway/handler_unixfs_file.go @@ -1,4 +1,4 @@ -package corehttp +package gateway import ( "context" @@ -13,15 +13,14 @@ import ( "github.com/gabriel-vasile/mimetype" "github.com/ipfs/go-libipfs/files" ipath "github.com/ipfs/interface-go-ipfs-core/path" - "github.com/ipfs/kubo/tracing" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) // serveFile returns data behind a file along with HTTP headers based on // the file itself, its CID and the contentPath used for accessing it. -func (i *gatewayHandler) serveFile(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, file files.File, begin time.Time) { - _, span := tracing.Span(ctx, "Gateway", "ServeFile", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) +func (i *handler) serveFile(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, file files.File, begin time.Time) { + _, span := spanTrace(ctx, "ServeFile", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) defer span.End() // Set Cache-Control and read optional Last-Modified time diff --git a/core/corehttp/lazyseek.go b/core/corehttp/gateway/lazyseek.go similarity index 98% rename from core/corehttp/lazyseek.go rename to core/corehttp/gateway/lazyseek.go index 2a379dc918a..0f4920fad43 100644 --- a/core/corehttp/lazyseek.go +++ b/core/corehttp/gateway/lazyseek.go @@ -1,4 +1,4 @@ -package corehttp +package gateway import ( "fmt" diff --git a/core/corehttp/lazyseek_test.go b/core/corehttp/gateway/lazyseek_test.go similarity index 99% rename from core/corehttp/lazyseek_test.go rename to core/corehttp/gateway/lazyseek_test.go index 49aca0a0e2a..09997a79796 100644 --- a/core/corehttp/lazyseek_test.go +++ b/core/corehttp/gateway/lazyseek_test.go @@ -1,4 +1,4 @@ -package corehttp +package gateway import ( "fmt" diff --git a/core/corehttp/gateway_test.go b/core/corehttp/gateway_test.go index 877ac9739ae..49fa519fb3d 100644 --- a/core/corehttp/gateway_test.go +++ b/core/corehttp/gateway_test.go @@ -651,28 +651,3 @@ func TestVersion(t *testing.T) { t.Fatalf("response doesn't contain protocol version:\n%s", s) } } - -func TestEtagMatch(t *testing.T) { - for _, test := range []struct { - header string // value in If-None-Match HTTP header - cidEtag string - dirEtag string - expected bool // expected result of etagMatch(header, cidEtag, dirEtag) - }{ - {"", `"etag"`, "", false}, // no If-None-Match - {"", "", `"etag"`, false}, // no If-None-Match - {`"etag"`, `"etag"`, "", true}, // file etag match - {`W/"etag"`, `"etag"`, "", true}, // file etag match - {`"foo", W/"bar", W/"etag"`, `"etag"`, "", true}, // file etag match (array) - {`"foo",W/"bar",W/"etag"`, `"etag"`, "", true}, // file etag match (compact array) - {`"etag"`, "", `W/"etag"`, true}, // dir etag match - {`"etag"`, "", `W/"etag"`, true}, // dir etag match - {`W/"etag"`, "", `W/"etag"`, true}, // dir etag match - {`*`, `"etag"`, "", true}, // wildcard etag match - } { - result := etagMatch(test.header, test.cidEtag, test.dirEtag) - if result != test.expected { - t.Fatalf("unexpected result of etagMatch(%q, %q, %q), got %t, expected %t", test.header, test.cidEtag, test.dirEtag, result, test.expected) - } - } -} diff --git a/core/corehttp/hostname.go b/core/corehttp/hostname.go index 39e857aadfb..cb6d7fbc5c3 100644 --- a/core/corehttp/hostname.go +++ b/core/corehttp/hostname.go @@ -13,6 +13,7 @@ import ( namesys "github.com/ipfs/go-namesys" core "github.com/ipfs/kubo/core" coreapi "github.com/ipfs/kubo/core/coreapi" + "github.com/ipfs/kubo/core/corehttp/gateway" "github.com/libp2p/go-libp2p/core/peer" dns "github.com/miekg/dns" @@ -225,7 +226,7 @@ func HostnameOption() ServeOption { if !cfg.Gateway.NoDNSLink && isDNSLinkName(r.Context(), coreAPI, host) { // rewrite path and handle as DNSLink r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path - ctx := context.WithValue(r.Context(), requestContextKey("dnslink-hostname"), host) + ctx := context.WithValue(r.Context(), gateway.DNSLinkHostnameKey, host) childMux.ServeHTTP(w, withHostnameContext(r.WithContext(ctx), host)) return } @@ -247,8 +248,6 @@ type wildcardHost struct { spec *config.GatewaySpec } -type requestContextKey string - // Extends request context to include hostname of a canonical gateway root // (subdomain root or dnslink fqdn) func withHostnameContext(r *http.Request, hostname string) *http.Request { @@ -257,7 +256,7 @@ func withHostnameContext(r *http.Request, hostname string) *http.Request { // Host header, subdomain gateways have more comples rules (knownSubdomainDetails) // More: https://github.com/ipfs/dir-index-html/issues/42 // nolint: staticcheck // non-backward compatible change - ctx := context.WithValue(r.Context(), requestContextKey("gw-hostname"), hostname) + ctx := context.WithValue(r.Context(), gateway.GatewayHostnameKey, hostname) return r.WithContext(ctx) } From 05a73def419a8e007f532b9c09c2fbfe6281fbad Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Fri, 27 Jan 2023 10:56:17 +0100 Subject: [PATCH 2/2] feat(gateway): use go-libipfs@main --- core/corehttp/gateway.go | 2 +- core/corehttp/gateway/README.md | 35 - core/corehttp/gateway/assets/README.md | 27 - core/corehttp/gateway/assets/assets.go | 203 --- core/corehttp/gateway/assets/build.sh | 14 - core/corehttp/gateway/assets/dag-index.html | 67 - .../gateway/assets/directory-index.html | 99 -- core/corehttp/gateway/assets/knownIcons.txt | 65 - .../gateway/assets/src/dag-index.html | 66 - .../gateway/assets/src/directory-index.html | 98 -- core/corehttp/gateway/assets/src/icons.css | 403 ------ core/corehttp/gateway/assets/src/style.css | 212 ---- core/corehttp/gateway/assets/test/main.go | 126 -- core/corehttp/gateway/gateway.go | 107 -- core/corehttp/gateway/handler.go | 1126 ----------------- core/corehttp/gateway/handler_block.go | 54 - core/corehttp/gateway/handler_car.go | 98 -- core/corehttp/gateway/handler_codec.go | 255 ---- core/corehttp/gateway/handler_ipns_record.go | 71 -- core/corehttp/gateway/handler_tar.go | 91 -- core/corehttp/gateway/handler_test.go | 28 - core/corehttp/gateway/handler_unixfs.go | 45 - .../gateway/handler_unixfs__redirects.go | 287 ----- core/corehttp/gateway/handler_unixfs_dir.go | 209 --- core/corehttp/gateway/handler_unixfs_file.go | 103 -- core/corehttp/gateway/lazyseek.go | 60 - core/corehttp/gateway/lazyseek_test.go | 136 -- core/corehttp/hostname.go | 2 +- docs/examples/kubo-as-a-library/go.mod | 2 +- docs/examples/kubo-as-a-library/go.sum | 6 +- go.mod | 10 +- go.sum | 8 +- 32 files changed, 15 insertions(+), 4100 deletions(-) delete mode 100644 core/corehttp/gateway/README.md delete mode 100644 core/corehttp/gateway/assets/README.md delete mode 100644 core/corehttp/gateway/assets/assets.go delete mode 100755 core/corehttp/gateway/assets/build.sh delete mode 100644 core/corehttp/gateway/assets/dag-index.html delete mode 100644 core/corehttp/gateway/assets/directory-index.html delete mode 100644 core/corehttp/gateway/assets/knownIcons.txt delete mode 100644 core/corehttp/gateway/assets/src/dag-index.html delete mode 100644 core/corehttp/gateway/assets/src/directory-index.html delete mode 100644 core/corehttp/gateway/assets/src/icons.css delete mode 100644 core/corehttp/gateway/assets/src/style.css delete mode 100644 core/corehttp/gateway/assets/test/main.go delete mode 100644 core/corehttp/gateway/gateway.go delete mode 100644 core/corehttp/gateway/handler.go delete mode 100644 core/corehttp/gateway/handler_block.go delete mode 100644 core/corehttp/gateway/handler_car.go delete mode 100644 core/corehttp/gateway/handler_codec.go delete mode 100644 core/corehttp/gateway/handler_ipns_record.go delete mode 100644 core/corehttp/gateway/handler_tar.go delete mode 100644 core/corehttp/gateway/handler_test.go delete mode 100644 core/corehttp/gateway/handler_unixfs.go delete mode 100644 core/corehttp/gateway/handler_unixfs__redirects.go delete mode 100644 core/corehttp/gateway/handler_unixfs_dir.go delete mode 100644 core/corehttp/gateway/handler_unixfs_file.go delete mode 100644 core/corehttp/gateway/lazyseek.go delete mode 100644 core/corehttp/gateway/lazyseek_test.go diff --git a/core/corehttp/gateway.go b/core/corehttp/gateway.go index 7d7674ef0ae..d5eccf73c27 100644 --- a/core/corehttp/gateway.go +++ b/core/corehttp/gateway.go @@ -5,11 +5,11 @@ import ( "net" "net/http" + "github.com/ipfs/go-libipfs/gateway" options "github.com/ipfs/interface-go-ipfs-core/options" version "github.com/ipfs/kubo" core "github.com/ipfs/kubo/core" coreapi "github.com/ipfs/kubo/core/coreapi" - "github.com/ipfs/kubo/core/corehttp/gateway" id "github.com/libp2p/go-libp2p/p2p/protocol/identify" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) diff --git a/core/corehttp/gateway/README.md b/core/corehttp/gateway/README.md deleted file mode 100644 index 102121d92de..00000000000 --- a/core/corehttp/gateway/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# IPFS Gateway - -> IPFS Gateway HTTP handler. - -## Documentation - -* Go Documentation: https://pkg.go.dev/github.com/ipfs/kubo/core/corehttp/gateway - -## Example - -```go -// Initialize your headers and apply the default headers. -headers := map[string][]string{} -gateway.AddAccessControlHeaders(headers) - -conf := gateway.Config{ - Writable: false, - Headers: headers, -} - -// Initialize a NodeAPI interface for both an online and offline versions. -// The offline version should not make any network request for missing content. -ipfs := ... -offlineIPFS := ... - -// Create http mux and setup gateway handler. -mux := http.NewServeMux() -gwHandler := gateway.NewHandler(conf, ipfs, offlineIPFS) -mux.Handle("/ipfs/", gwHandler) -mux.Handle("/ipns/", gwHandler) - -// Start the server on :8080 and voilá! You have an IPFS gateway running -// in http://localhost:8080. -_ = http.ListenAndServe(":8080", mux) -``` \ No newline at end of file diff --git a/core/corehttp/gateway/assets/README.md b/core/corehttp/gateway/assets/README.md deleted file mode 100644 index 25d1a35e80d..00000000000 --- a/core/corehttp/gateway/assets/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Required Assets for the Gateway - -> DAG and Directory HTML for HTTP gateway - -## Updating - -When making updates to the templates, please note the following: - -1. Make your changes to the (human-friendly) source documents in the `src` directory. -2. Before testing or releasing, go to `assets/` and run `go generate .`. - -## Testing - -1. Make sure you have [Go](https://golang.org/dl/) installed -2. Start the test server, which lives in its own directory: - -```bash -> cd test -> go run . -``` - -This will listen on [`localhost:3000`](http://localhost:3000/) and reload the template every time you refresh the page. Here you have two pages: - -- [`localhost:3000/dag`](http://localhost:3000/dag) for the DAG template preview; and -- [`localhost:3000/directory`](http://localhost:3000/directory) for the Directory template preview. - -If you get a "no such file or directory" error upon trying `go run .`, make sure you ran `go generate .` to generate the minified artifact that the test is looking for. diff --git a/core/corehttp/gateway/assets/assets.go b/core/corehttp/gateway/assets/assets.go deleted file mode 100644 index 2e442dd1347..00000000000 --- a/core/corehttp/gateway/assets/assets.go +++ /dev/null @@ -1,203 +0,0 @@ -//go:generate ./build.sh -package assets - -import ( - "embed" - "io" - "io/fs" - "net" - "strconv" - - "html/template" - "net/url" - "path" - "strings" - - "github.com/cespare/xxhash" - - ipfspath "github.com/ipfs/go-path" -) - -//go:embed dag-index.html directory-index.html knownIcons.txt -var asset embed.FS - -// AssetHash a non-cryptographic hash of all embedded assets -var AssetHash string - -var ( - DirectoryTemplate *template.Template - DagTemplate *template.Template -) - -func init() { - initAssetsHash() - initTemplates() -} - -func initAssetsHash() { - sum := xxhash.New() - err := fs.WalkDir(asset, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - if d.IsDir() { - return nil - } - - file, err := asset.Open(path) - if err != nil { - return err - } - defer file.Close() - _, err = io.Copy(sum, file) - return err - }) - if err != nil { - panic("error creating asset sum: " + err.Error()) - } - - AssetHash = strconv.FormatUint(sum.Sum64(), 32) -} - -func initTemplates() { - knownIconsBytes, err := asset.ReadFile("knownIcons.txt") - if err != nil { - panic(err) - } - knownIcons := make(map[string]struct{}) - for _, ext := range strings.Split(strings.TrimSuffix(string(knownIconsBytes), "\n"), "\n") { - knownIcons[ext] = struct{}{} - } - - // helper to guess the type/icon for it by the extension name - iconFromExt := func(name string) string { - ext := path.Ext(name) - _, ok := knownIcons[ext] - if !ok { - // default blank icon - return "ipfs-_blank" - } - return "ipfs-" + ext[1:] // slice of the first dot - } - - // custom template-escaping function to escape a full path, including '#' and '?' - urlEscape := func(rawUrl string) string { - pathURL := url.URL{Path: rawUrl} - return pathURL.String() - } - - // Directory listing template - dirIndexBytes, err := asset.ReadFile("directory-index.html") - if err != nil { - panic(err) - } - - DirectoryTemplate = template.Must(template.New("dir").Funcs(template.FuncMap{ - "iconFromExt": iconFromExt, - "urlEscape": urlEscape, - }).Parse(string(dirIndexBytes))) - - // DAG Index template - dagIndexBytes, err := asset.ReadFile("dag-index.html") - if err != nil { - panic(err) - } - - DagTemplate = template.Must(template.New("dir").Parse(string(dagIndexBytes))) -} - -type DagTemplateData struct { - Path string - CID string - CodecName string - CodecHex string -} - -type DirectoryTemplateData struct { - GatewayURL string - DNSLink bool - Listing []DirectoryItem - Size string - Path string - Breadcrumbs []Breadcrumb - BackLink string - Hash string -} - -type DirectoryItem struct { - Size string - Name string - Path string - Hash string - ShortHash string -} - -type Breadcrumb struct { - Name string - Path string -} - -func Breadcrumbs(urlPath string, dnslinkOrigin bool) []Breadcrumb { - var ret []Breadcrumb - - p, err := ipfspath.ParsePath(urlPath) - if err != nil { - // No assets.Breadcrumbs, fallback to bare Path in template - return ret - } - segs := p.Segments() - contentRoot := segs[1] - for i, seg := range segs { - if i == 0 { - ret = append(ret, Breadcrumb{Name: seg}) - } else { - ret = append(ret, Breadcrumb{ - Name: seg, - Path: "/" + strings.Join(segs[0:i+1], "/"), - }) - } - } - - // Drop the /ipns/ prefix from assets.Breadcrumb Paths when directory - // listing on a DNSLink website (loaded due to Host header in HTTP - // request). Necessary because the hostname most likely won't have a - // public gateway mounted. - if dnslinkOrigin { - prefix := "/ipns/" + contentRoot - for i, crumb := range ret { - if strings.HasPrefix(crumb.Path, prefix) { - ret[i].Path = strings.Replace(crumb.Path, prefix, "", 1) - } - } - // Make contentRoot assets.Breadcrumb link to the website root - ret[1].Path = "/" - } - - return ret -} - -func ShortHash(hash string) string { - if len(hash) <= 8 { - return hash - } - return (hash[0:4] + "\u2026" + hash[len(hash)-4:]) -} - -// helper to detect DNSLink website context -// (when hostname from gwURL is matching /ipns/ in path) -func HasDNSLinkOrigin(gwURL string, path string) bool { - if gwURL != "" { - fqdn := stripPort(strings.TrimPrefix(gwURL, "//")) - return strings.HasPrefix(path, "/ipns/"+fqdn) - } - return false -} - -func stripPort(hostname string) string { - host, _, err := net.SplitHostPort(hostname) - if err == nil { - return host - } - return hostname -} diff --git a/core/corehttp/gateway/assets/build.sh b/core/corehttp/gateway/assets/build.sh deleted file mode 100755 index 531bbfc0244..00000000000 --- a/core/corehttp/gateway/assets/build.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -set -euo pipefail - -function build() { - rm -f $1 - sed '/ ./base-html.html - (echo "") > ./minified-wrapped-style.html - sed '/<\/title>/ r ./minified-wrapped-style.html' ./base-html.html > ./$1 - rm ./base-html.html && rm ./minified-wrapped-style.html -} - -build "directory-index.html" -build "dag-index.html" diff --git a/core/corehttp/gateway/assets/dag-index.html b/core/corehttp/gateway/assets/dag-index.html deleted file mode 100644 index 5bba8f5c0d5..00000000000 --- a/core/corehttp/gateway/assets/dag-index.html +++ /dev/null @@ -1,67 +0,0 @@ - -{{ $root := . }} - - - - - - - - - - - - - - - - - -{{ .Path }} - - - -

-
-
-

CID: {{.CID}}
- Codec: {{.CodecName}} ({{.CodecHex}})

-
-
- - - - - - - -
-

Preview as JSON
(application/json)

-
-

Or download as: -

-

-
-
-
- - diff --git a/core/corehttp/gateway/assets/directory-index.html b/core/corehttp/gateway/assets/directory-index.html deleted file mode 100644 index d861cb65700..00000000000 --- a/core/corehttp/gateway/assets/directory-index.html +++ /dev/null @@ -1,99 +0,0 @@ - -{{ $root := . }} - - - - - - - - - - - - - - - - - -{{ .Path }} - - - - -
-
-
- - Index of - {{ range .Breadcrumbs -}} - /{{ if .Path }}{{ .Name }}{{ else }}{{ .Name }}{{ end }} - {{- else }} - {{ .Path }} - {{ end }} - - {{ if .Hash }} -
- {{ .Hash }} -
- {{ end }} -
- {{ if .Size }} -
-  {{ .Size }} -
- {{ end }} -
-
- - {{ if .BackLink }} - - - - - - - {{ end }} - {{ range .Listing }} - - - - - - - {{ end }} -
-
 
-
- .. -
-
 
-
- {{ .Name }} - - {{ if .Hash }} - - {{ .ShortHash }} - - {{ end }} - {{ .Size }}
-
-
- - diff --git a/core/corehttp/gateway/assets/knownIcons.txt b/core/corehttp/gateway/assets/knownIcons.txt deleted file mode 100644 index c110530ea59..00000000000 --- a/core/corehttp/gateway/assets/knownIcons.txt +++ /dev/null @@ -1,65 +0,0 @@ -.aac -.aiff -.ai -.avi -.bmp -.c -.cpp -.css -.dat -.dmg -.doc -.dotx -.dwg -.dxf -.eps -.exe -.flv -.gif -.h -.hpp -.html -.ics -.iso -.java -.jpg -.jpeg -.js -.key -.less -.mid -.mkv -.mov -.mp3 -.mp4 -.mpg -.odf -.ods -.odt -.otp -.ots -.ott -.pdf -.php -.png -.ppt -.psd -.py -.qt -.rar -.rb -.rtf -.sass -.scss -.sql -.tga -.tgz -.tiff -.txt -.wav -.wmv -.xls -.xlsx -.xml -.yml -.zip diff --git a/core/corehttp/gateway/assets/src/dag-index.html b/core/corehttp/gateway/assets/src/dag-index.html deleted file mode 100644 index 7a42ef6bed7..00000000000 --- a/core/corehttp/gateway/assets/src/dag-index.html +++ /dev/null @@ -1,66 +0,0 @@ - -{{ $root := . }} - - - - - - - - - - - - - - - - - - - -{{ .Path }} - - - -
-
-

CID: {{.CID}}
- Codec: {{.CodecName}} ({{.CodecHex}})

-
-
- - - - - - - -
-

Preview as JSON
(application/json)

-
-

Or download as: -

-

-
-
-
- - diff --git a/core/corehttp/gateway/assets/src/directory-index.html b/core/corehttp/gateway/assets/src/directory-index.html deleted file mode 100644 index 109c7afbf44..00000000000 --- a/core/corehttp/gateway/assets/src/directory-index.html +++ /dev/null @@ -1,98 +0,0 @@ - -{{ $root := . }} - - - - - - - - - - - - - - - - - - - -{{ .Path }} - - - -
-
-
- - Index of - {{ range .Breadcrumbs -}} - /{{ if .Path }}{{ .Name }}{{ else }}{{ .Name }}{{ end }} - {{- else }} - {{ .Path }} - {{ end }} - - {{ if .Hash }} -
- {{ .Hash }} -
- {{ end }} -
- {{ if .Size }} -
-  {{ .Size }} -
- {{ end }} -
-
- - {{ if .BackLink }} - - - - - - - {{ end }} - {{ range .Listing }} - - - - - - - {{ end }} -
-
 
-
- .. -
-
 
-
- {{ .Name }} - - {{ if .Hash }} - - {{ .ShortHash }} - - {{ end }} - {{ .Size }}
-
-
- - diff --git a/core/corehttp/gateway/assets/src/icons.css b/core/corehttp/gateway/assets/src/icons.css deleted file mode 100644 index dcdbd3cd9e2..00000000000 --- a/core/corehttp/gateway/assets/src/icons.css +++ /dev/null @@ -1,403 +0,0 @@ -/* Source - fileicons.org */ - -.ipfs-_blank { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAWBJREFUeNqEUj1LxEAQnd1MVA4lyIEWx6UIKEGUExGsbC3tLfwJ/hT/g7VlCnubqxXBwg/Q4hQP/LhKL5nZuBsvuGfW5MGyuzM7jzdvVuR5DgYnZ+f99ai7Vt5t9K9unu4HLweI3qWYxI6PDosdy0fhcntxO44CcOBzPA7mfEyuHwf7ntQk4jcnywOxIlfxOCNYaLVgb6cXbkTdhJXq2SIlNMC0xIqhHczDbi8OVzpLSUa0WebRfmigLHqj1EcPZnwf7gbDIrYVRyEinurj6jTBHyI7pqVrFQqEbt6TEmZ9v1NRAJNC1xTYxIQh/MmRUlmFQE3qWOW1nqB2TWk1/3tgJV0waVvkFIEeZbHq4ElyKzAmEXOx6gnEVJuWBzmkRJBRPYGZBDsVaOlpSgVJE2yVaAe/0kx/3azBRO0VsbMFZE3CDSZKweZfYIVg+DZ6v7h9GDVOwZPw/PoxKu/fAgwALbDAXf7DdQkAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-_page { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmhJREFUeNpsUztv01AYPfdhOy/XTZ80VV1VoCqlA2zQqUgwMEErWBALv4GJDfEDmOEHsFTqVCTExAiiSI2QEKJKESVFFBWo04TESRzfy2c7LY/kLtf2d8+555zvM9NaI1ora5svby9OnbUEBxgDlIKiWjXQeLy19/X17sEtcPY2rtHS96/Hu0RvXXLz+cUzM87zShsI29DpHCYt4E6Box4IZzTnbDx7V74GjhOSfwgE0H2638K9h08A3iHGVbjTw7g6YmAyw/BgecHNGGJjvfQhIfmfIFDAXJpjuugi7djIFVI4P0plctgJQ0xnFe5eOO02OwEp2VkhSCnC8WOCdqgwnzFx4/IyppwRVN+XYXsecqZA1pB48ekAnw9/4GZx3L04N/GoTwEjX4cNH5vlPfjtAIYp8cWrQutxrC5Mod3VsXVTMFSqtaE+gl9dhaUxE2tXZiF7nYiiatJ3v5s8R/1yOCNLOuwjkELiTbmC9dJHpIaGASsDkoFQGJQwHWMcHWJYOmUj1OjvQotuytt5nHMLEGkCyx6QU384jwkUAd2sxJbS/QShZtg/8rHzzQOzSaFhxQrA6YgQMQHojCUlgnCAAvKFBoXXaHfArSCZDE0gyWJgFIKmvUFKO4MUNIk2a4+hODtDUVuJ/J732AKS6ZtImdTyAQQB3bZN8l9t75IFh0JMUdVKsohsUPqRgnka0tYgggYpCHkKGTsHI5NOMojB4iTICCepvX53AIEfQta1iUCmoTiBmdEri2RgddKFhuJoqb/af/yw/d3zTNM6UkaOfis62aUgddAbnz+rXuPY+Vnzjt9/CzAAbmLjCrfBiRgAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-aac { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnhJREFUeNp0Uk1PE0EYftruVlvAUkhVEPoBcsEoLRJBY01MPHjCs3cvogcT/4qJJN5NvHhoohcOnPw4YEGIkCh+oLGBKm3Z7nZ3dme2vjOhTcjiJJvZzPvOM8/HG2q325Dr3kLp7Y1ibpIxjs4KhQBZfvV6s7K5Vb0bjeof5ZlcGysP1a51mifODybvzE8mzCbrAoTDIThMoGXZiZ4YSiurf+Z1XeuCqJ7Oj+sK3jQcNAmg8xkGQ71mYejcAB49vpmeuzJccl0+dUj6KIAvfHCPg3N+uAv4vg9BOxcCmfEzuP/genpmeqhEMgude10Jwm+DuUIyUdTlqu2byoMfX/dRermBeExHsTiWNi3+lMpzRwDki8zxCIATmzbevfmClukiP5NFhJgwkjeRTeLShdOoVJqnAgwkgCAZ6+UdLC9twjQZ8pdzioFkZBHY3q6B3l4dJEEEPOCeD4cYVH7Xsf15F+FImC775INAJBJSkVoWo0QY9YqgiR4ZZzRaGBkdwK3bFxGLRZUfB3Rm2x4x9CGtsUxH9QYkKICDFuLxKAozGZwdTqBRs2FbLlXbiPdECMCHadj/AaDXZNFqedCIvnRcS4UpRo7+hC5zUmw8Ope9wUFinvpmZ7NKt2RTmB4hKZo6n8qP4Oq1HBkKlVYAQBrUlziB0XQSif4YmQhksgNIJk9iaLhPaV9b/Um+uJSCdzyDbGZQRSkvjo+n4JNxubGUSsCj+ZCpODYjkGMAND2k7exUsfhkCd+29yguB88Wl7FW/o6tT7/gcXqAgGv7hhx1LWBireHVn79YP6ChQ3njb/eFlfWqGqT3H3ZlGIhGI2i2UO/U/wkwAAmoalcxlNA1AAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-ai { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAk5JREFUeNpsU01vElEUPTPzZqBAQaSFQiJYUmlKYhoTF41L3Tbu/Q/+AvsX3Bp/gPsuWLrqyqQ7TUxMtAvF1tYGoXwNw7wv7zwYgtKX3Lw379575p5z77O01ohW+/DVh8zj7aYKhflGdG9ZsGwLNydffgVfr19YHvsEa+Zu/nxndob5StQK+dyzvZzyw/gKlmMj7IygFM+xvNcanp4/t5dAomXHBy2UUBOO2MAl/B9/cPb6PULuoHx0WM0e3GvpUOxD3wZAJWutZqYUYmqpSg5OMgH3YQObL59W0/ullpryR3HegkKEqiWBSGV4R3vQ7sIhScTZFTpHx3A215B5sluVY/WWMg7+ATB/lcLsKpTonHzD+OMFEuTz8ikkt9Kwt9YJZB38cpBdoQAZJdLvCGByfoPB6Xdk90pYy6Xg3c/DaWwArg09DaG5lCsUFN0pckZAojdC8m4auBqaALuSgez7VB1RtDSUWOQvUaBLFUzJBMJ2DwmPgd1Jwm0WoSgJfjDvrTKxtwAIyEkAOQ5hU//Zdg5uowDlUNMnwZLW0sSuUuACYhwQRwFvJxupCjEYUUccOkoaKmdOlZnY1TkgAcXAhxhOwLsDsHoN3u4O5JTDfVCH6I9nfjId3gIgSUATFJk/hVevGtOMwS0XwQ3AzB/FrlKg8Q27I2javVoZrFgwD4qVipAEyMlnaFArzaj/D0DiMXlJAFQyK2r8fnMMRZp4lQ1MaSL5tU/1kqAkMCh2tYI+7+kh70cjPbr4bEZ51jZr8TJnB9PJXpz3V4ABAPOQVJn2Q60GAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-aiff { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAohJREFUeNpkU9tqE1EUXZmZpE3aTBLbJFPTtFURtSCthr7UCyKKFJ/9An3og6Ag/oXfoUj7og9asCBYKT6UIPHaWtpq7NU2aZK5z5wZ9xxMpMwZDuewz9prr32ZiO/7CNaDx3OLt6fOjBqGg/aKRCIInp8+KzfKH7fudnVF58nE16el+/yU2mBFSWZKpWJKVc0OgUBo02K4NDmU6o75Mx+Wdu9IUXFeiOA/pn1xHeYaugVDdzpbp91qGlAKGTx8dC19/Wpxhjnsxj/RRwk85hGJC9d1O6fneWAuoztDYSSLe9OT6SuXB2ccx73Z9uukwDwfls1g0xZIY/Ad/Gnyt/XVfbyYrSDRE8PExHB6/8B6QuaxIwRBFMt0iIAiMx+LCys8jfGJEUik2WpZOD2SQf9oDtVqQwopCAiY66FS/om3b75CVS2MlU7AJ2WiJBCZjZ2dJuRkDJZFwFAR7UCBja3fNfxY2YEoCtRCj9em3Tpds6FpJseGCBxS0GgYGBzqw62p84gnYnAI2CSbSbPhEpFAaE2zODaUAlWWwDoS5DheGqbWpVE/0CmqCY9qkEyINBceb2uADRNQ8bSWAVVzIFKomCQim+0luS4yKYlsHlRyZo7EsSEC23K5vAsXh/H92zZkuRvxeBS5nEx2yp2KqhxPoV5TYS/8CtdApylM9sZQKKSQzyeRTseRV2QoAzIYY8jme5DN9fI0dQoUIjANGydP9VM7PZw9p/AiBpNYrdbw/t0yTJqRtdU9UrfJCUMpSJIgbWzsYe51BcViHzLHeqCRqhZ1YX1tFwNfZBxS9O3NWkAcHqR606k/n/3coKAoV/Y7vQ/OYCZevlrmv3c0GsFh06u3/f4KMABvSWfDHmbK2gAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-avi { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAm1JREFUeNpsU8tu00AUPXZcN0nzTpq2KQ3pAwkIAnWHqCoeexBb+AQ+ABZ8A2s+AIkdm266QUJIFWKBkHg1KpRHi5omJGkbJ3bGHj+4M1EQrTvSyGPPueeec++1EgQBxHp+/9mbyuriRZdxjJaiKBD3W+u1+p9a856max+gDO8ebT+WT20Ezi9NZi/crqadvn2MQBAGfpCOpqNru2937vxPIpY6Onjccx3Twck9MBiSU0ncfHirXFmZX3Md9wqCUwiEVN/zaQfHt0vfbBe5uQyuPVgpl5Zn11ybL4/i/lkICOw5niQRGQShoiqI6Bo43W2ub8n3hRtLZT7gTynk6gkCX9gAOxpAnxhHZDwC1/aI1EViJolu/QhKRMHZ1UX0Gr1USIEn5FPWHy+/wTokkrQOq2vBaHZBN4hmY9Jwfr4An/teiEB45ZZDwDiMhoExT0N+sYDCuUkkplLIlXP4/XEXdo+RUhdhBSSfUwtVTUG8MIHK9QVqI7D/uY6vr2pwmCPrkz+Tk9gwARWQ9WxppbXZhNnpw+ya4A5HZi6L4lIR8WyCcL6sTZiAWjWgAmpxkn5+kqTamK6WkCwmERmLDLvjB0ML9ikWXPLFuozYOap3L8HYN6DHdbS/d5CeTVBndBz87FCBLYkNTyIjBQemnIEsSY5lYrK1+UoWcToLMjEHAyIQ2BCBSx/NVh+ZUhrqmEqBebS3WyhdLg0zt/ugAaIklsSGLHCLa6zDMGhZ2HjyGsnpFPqNHnY2fmHv3R5SMymYbROszSQ2ROAY9qHiofvlxSc5xsKKqqnY3diRE9h4X5d/pzg7lnM4ivsrwADe9Wg/CQJgFAAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-bmp { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmZJREFUeNp0U+1rUlEY/13v9YV0vq2wttI5CdpL9aEGBZUDv0df668I6n+or0UQ/RuuD0EgVDAZrsKF4AR1a6COKW5qXvXec27PuVeda3bgcF6e8/ye5/d7niMZhgExnK9fbTrm5pbBGMZDkgCyq+VyhTUaT6Eo2ZHJePPWXJXRhez3B1yxmM/QdctXUSCgtV4Py4CvY3cky4e1x5DlLCaGbbzjXDcousG5OQe5HPRSCQPK4PpsEM/XH4WvhS4noeu3JwHGGRiULhsMoKZS4I0GtEIB9mgULJGA0+9DPBpBT7sffvf1W/Lg6OgJufw8C0CRGEXWazUwiiyFQjA8bsjVKjaJzovMD/Q5gxyJhG2cvyeXe2cAuADQNGBmBvLaGuTFRaDfh31lBTWi9pumjbK0B4JQul3vOQpM8JdskOLrdCvDcDjAsjtg5TIkoiKLaokMNR2cnZbqNAMycqG7XbHKR2fMzwO/dsxSwu0BiBJsNsv2LwAJAJCI5ux2gXYbqNetcz5PoORI1cDS0n8AxGW7A+zvEYBKZ2ZlcsEtJLbedMjePBaCTQMghx45ulyWkzxMVUQ2RMQhLfFO16YAqCrixPnm6iqKrRb2W23EfF4cUNSrHg90cr7hDyB33MTnSmUKALVs4uIlROjxg+AsPhGVl3fuIl2tIOB0Ya91gkOi9mxhAal0ekork1ic/kGLBORMxy2K1qS9V1ZQbNThIj2EGh+2tsyOnSai8r1UxMNIBB+LRTTULr4Uds0K1tU/uOLxIrmbNz8XXSrnASSpubG9fbKRyVh1n/zSw29t9oC1b47MfwUYAAUsLiWr4QUJAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-c { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAcxJREFUeNqEUk1rE0EYfmZnkgoJCaGNCehuJTalhJZSUZB66a0HwXsP/Qn+FM+9+hty0LNYCr2I7UVLIW0Fc0hpQpSS7O7MrO9MspuvVV8YMnk/nn2e5x0WRRFMvP/w6WSz5jbi/9NxfP693Wp3DrJCnMW5d28P7a+IE15lufR8o1ZEStwPhkWHsWbrZ+eNEPxsuubEF6m0TBv2Q4liPofXuzveulttSqW2UwH+GjqC0horpSL2njU89+FyMwjlTlxOJMTa9ZQHzDQIjgwdom9zLzfXPc75kbnOAswBJTlC2XrqQRMLxhi442DgB4UFBhgPpm3B5pgBHNUUxQKAHs8pHf3TEuFMetM9IKr/i2mWMwC0SnuSFTG2YKyppwKYVdGO7TFhzBqGIenVeLCUtfURgErucx5ECKREKBU4d3B718PHz6cICGT/1Qs8qpQtGOdyhtGEARWDQFqQJSeDL98u4VbLaKw9IRAJPwjtoJGlVAoDQ800+fRFTTYXcjlcXN2g++s36p5Lzzlve1iEROa8BGH1EbrSAeqrjxEqicHQt8/YSDHMpaNs7wJAp9vvfb287idboAVkRAa5fBYXP9rxO4Mgf0xvPPdHgAEA8OoGd40i1j0AAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-cpp { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAfJJREFUeNqEUs9PE0EU/mZ2WgqpXX+QIDFdalVslh8NlAOQaOKFAwfvHvwT/FM8e/U/MOnBmwcj8WD0ACEGghIkbU0baaEthe3OTJ0ZWV26q37JZt68ee/b9733yGAwgMbL12/fz+azbnAPY2Nrt7Zfqz9JMrYZ+J4/e2pOFjiciRvXlgp5GzHonXk2o6S8V6k/TjBrM/xGA4MLyeOSPZ8jkx7D+uqCU3Amy1yIYizB36AlCSkwfjWDR4uu40yMl/s+XwjeWThQQ4Z6QNSnSkYykcDXasP4lmfvOZTSF9q8TDBEFPbN5bOqCglCCCxK0TvvZyIV4CIxbgpC+4gm/PUmFCIE8iJPyME/e8Lon9j4HvyHYLjKSwRCSEUgf9+15mFbx8QS6CZJMzJ9SlBCwX3fJDLG4PX7ykcwkmQmJtpEhWa7g1dvNlSwjwelebz7tAXLolh0p/Fxe9fErK2WDFGEgKjxfNjegX0lDTc/heNuF99/HGEslcKXwyoazWNDdlCr6+DoJgrBzdI0T9rYO6yg2zszMlaKM3Dv5OBzbuyZuzm1B16U4Nzz2f3cFOx0Gq12F9cztpExncsqYoaHpSIKtx0zJdVIFpHQ6py29muNk1uTN829o/6SHEnh80HFaE6NjmLnWxUJy1LyTltB3k8BBgBeEeQTiWRskAAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-css { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAk1JREFUeNpsUktvUlEQ/u5DoCLl/RAKKKUvWmIxjYntQtcu3LvwJ/hTXLt16coFC2PsojEaMKZtCqFaTdGmjbS0CG3By+vei3OOBSGXSU7uzNyZ78z3zRF6vR6YvXzzPrMUCyf68bB9zO+VfpROn5hkOdfPPX/2lH/lfiLidztX5mN2jLGG0rKLENIE8liWpdzwP7HvqJqujmvudFU4bFY8Wk1FZsOBtKppd8YCDNu77CZevd3gflfTUFcUhP0ePLibiIR9rjSBpgwAfe4dVcV6dhtep4PH5msylGYLrzeybErcT85FYiH/CyPAf74gObC2vMhzsiRhPhpC6eQUM+EA1pJzILEnjRSuJsju7MJqsUCSRei6Dp3yXqcdGlHZ/rLPazQWGCn8+6YW4pAkEW0SjzUzanWlCa/LgcR0lNfovTEi6lcIkzesnM/R8RlN0INGp3h4DHoDsE5YRvQyiKiRSMzikRAOS2WoqoZWu41K7RwzlOOAVDMMMHhIGvFlRxJFrKYW0ep0IYgC3SDh4b1lTJjNfENsrazOAMAw680mPuW+8lFno1P4XDigRhOiwQAyJK7TbsNS/PaA7giAIAhYz2yRgBIfsVA8wIetPG6FAqhdNrC5u0f+TUyHgyMTDDToEt/ftQsEvW4EPG5OZcrvw0mlimarTXkPfpXPcNlQoGtjACgpryQXsPNtH/nvRXqBJpoKHMzGNkNB0Odls7LNyAYKpUq1dt1iuvB7fRDp9kr9D1xOFwkpoksXusmXaZWFn0coV89r/b6/AgwAkUENaQaRxswAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-dat { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAfVJREFUeNqMU01PE1EUPe/Na0uptmlASg3MoiZgCA3hQ8PHAjbqwsS9C3+CP8W1W/+BSReyYUPwI4QAVkAgUEgIbVIg1FZb2pl5b3zv2cHBjsaTTOa+e989OffcGeK6LhTevFv+OJoZHPHOfrz/sl86KpWfhxnLe7lXL1/oN/MSZqonOXU/k0AA6lfNhEFIrlAsP2PMyPtr1AscLpyg5pbtIHErhqez4+awmc45nI8FEvwNaiQuBHqTcSxMjJhmX0/Osp1xr878FxWEzwMinxAzEA4xFIpnOjedHTKpYbxW4U2CP4j8uWxmUKsghMCgFI2mFe9QgHZj0Ba4yhFF+KvGJToIRLuPC/efnjD6+26wB1Lq/xgbSCBXKeWJG/OTdky8cWTdT3C9RmWSGk2XCLlWo4xTNbfN5qh7PpXM72GjZeHt0gpq9QbmH4whGb+NpU/reDQ7hcWVVXxvXOHxzCQopQEKXKEbL6o1ZIcy+LC5g62DY2zsHeC0fA4zndIrHOjvg2XbAQRSfsuy9XxC2qzi/H5B6/68W0AsGkW0KyJPBLbDO0fg3JX/CUM81i0bD6WKe6j9qOPJ3EMcF0tSNsFA6g6alqW+VtZBUL78Vtk+Oqne7U9rs5qOQCjSheJFBeFIFOfVujSUYu3rIc4uqxWv76cAAwCwbvRb3SgYxQAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-dmg { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAn9JREFUeNpsU01rE1EUPe9lkk47yWTStCmtNhFSWxos2EXVhSsRcasuxYV05V8Qf4DgD/AvCK5EV1oFI7iUBqmCNdDvppq2mWSSzEzy3vPOpFFq+uDNfR/3nnvueXeYUgrBWH1/9/NE7k5BKRnuRcfF2qdnmJq9DeF9tQ+2isuMsxXGWHh/a1mEVsPJSI5fSU3OPEj291IIlN49RXz0KqzEQjIeZS/L5Y/3wPGhDxIM/i/A7fZWgVG0t5EaG0ZUa0JGM8gvPrZmLt58QYwv91mfAqCIE0sAqgumBFITGQzpUYhuF0KfRa7waDyXXXolpVrsh/0tgSLDr5I+wUZo1UHCSkAficPzY6juFSmbRPrC/azjq+fkcO00gAqoU7B0ETKkfWbuCTjTYeq5oESAauexcTScX+ZACWFm0YQSLZKhHdr67+/wW0e0dgjYo3sCEXXybYtBDVSHLp2es3IpsILS24c42lkBg6DzRjgRzCDZ/xr0GNRJwwYiWgzt+hYMawleu0V3wbkT+kUirOc7IGJAz68R/Qak1BAlx3hqASPGBJRXpXOv58dkz3eAgQoOm4hyj57NgZm0MHvpBmK6QdUdg/DAg9cRkhicBSDaKJdeo1bdxmR2DtWDDUxl51HZ+QHTysD3XdQO95Gfv06aeGcAdBrY3Chi8lwO3768QWX7J5q1XWyVSxgajiOXLyBG2hzurRKV9lmt7ISNkkjo6HhNyjoK+2gXRsKE57ZIE2ot10Z1fz0Ue4ABVw3NMjnW14rInh8jTYywoTg3EOFpOM4mXNfH9PQUfGlrAwBOs3I8ljbtuMWhRWzIIPrkn+GcYcgIWEowbZ+0qB334/4IMADESjqbnHbH0gAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-doc { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAppJREFUeNpsU79PFEEU/mZ39vZu77g7DokcP04BBSUmiEKCSCxs7Ei00JAYO2NlTKyMrX+CJhaGwopSQ0dMtFEsbDRBgiZEQIF4IHcg+2t2Z8eZ5QDlnM1mZ9+8973vfe8NEUJArfSNhzPG0VIfeIiDRSDkw1cWVt3N8rhG6SdSO2Gvn8dfuueqZwuNZqk3Jxg7iNcIfBbgXD6ZC8u5qffzX8eoYeyDxC77uygKhcouovgVUQj1H4YB2ovNuD9+tTTU0zMVBmG/+C8AIYh8F361DL/yE5HnADKYlVdg6MDAmW7cuz5WGuw+PsWDYGAvbL8ECFUt4K7/AHd/I9c7BLaxinD2Ld5Zo7g78RLuRhlBS2cpWbGfStfhfwCEpK0nUjCbWuGsLciSOELPhkq/YgdY3l6HsLfRcLYf+pHNbH0JigEPkLAyMsiEJ7NrqQzM1i7wyhoMZqOhvQs6Z0ovXgdAJACRoulEg5HOwrOroKk0zOY2BDtVpTF0CU6kLkQJXa+BNEoG0lMSsBBKQXWNQktmoGcaYeSaQCIVWOvUYQAiWZFQtk5mSMoSzEILtBrTfEcviC5bwVwQmoh96wA0ic5dB57ngeoaTIPCdb34zDITYNLOOIeVSsW+dQC+7+NSWx6jJ4tY/rWNV7PfcGv0tBoPTM7M4eKJVgx2FTE9u4QPS6x+kHzfw/mOAjarW2hJG3hy8zIceweuY+PRtREMdzbjzcd5WBqPB6xeRGUMGRzHjWvMmxQ7tiOF1JBN6FiTd6Sy9RuFbHpX7MMMqOD088Ii+op5OUAO7jyeRGfBwrF8Cg8mXuDL4neMXzgFwhwZz+hf7a9d5yu3Z6DTPjVQIY9k7erO7Y63Lvc8ErEeyq6JaM6efjai4v4IMABI0DEPqPKkigAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-dotx { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAndJREFUeNpsU01rE1EUPTPzJk0y+WhMStW2qdVWxUVEQUF0I+4ELQiC7lz4N9z0T+hG9wrdZKUgLqulhrbSag1CKpT0g7RpYjqZmffle5NEKdMHlzfvvXvPPffcO4aUEno9f3Vt4dTp+BXOe+fB0u/NbVpv7h89NU1j1TCM8H7+xY9wJwPHZMbOjRadLAvE/2gToJTiTPx89k+OlVd/LT+0TPIPpO/SzyQk40xCMxBSZ9Z3CoAx5DOjeHT7SbE0XSpzwa8OWB9jINELolQg8AR0EgUKn1PIlIWpkUt4cPNxkTOU12trs8p95RiAXpqaztqou8q6SKQJJmZSqGwsodFsIJk1kcyLYv7IeafcLx4HUNkFF4jFTExMZ0B9DrfD4HUEusYhWs4GPEJg5wly/tBYRIOeDhpEwlS34xcyajdQr3UwOT2MlJOEBRuGNHWp9AQRVXDfQiFV/U5GBSiQ5p6ngBEa5z3fiIhC6g6IMDBwOdoHPkYnHPVyhN0tF7E4QSpr94CEOKELffq+y9Bq+DCJ7rWBoQQBVbPR2O6G4OlsLASJMtCZfQqm0NP5IVWnamdAkUxbyuIYtD7wWegb0YAzAVMkkI6NwPM9xEwHloyDGAmk7AKS9rAS0FKOdugbYeAHPu7OPEM+MY7q3hIKqTFQHmC3XcONc/fxdfMDrk/ew/edzyhvvTmBAddocVRqH3Frahau56qpZDho7+PnTgXffi/gbHYmLEvPSIQBp5JU62sYz13G609zKBXvoOMdYn2zgm7Xg2MVML/4Eu3uPgxhk2gXmNl8v/i2pcXTP8tKdTEcbWLZqDQXwu/l6pfwbEnSGsT9FWAA4mdHv2/9YJ4AAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-dwg { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAoFJREFUeNpsU0tPE2EUPfOg006hD4rQh8WgbCSwkKgbF2owujaCiQsXxpX+D6MmbtXEsHCLmIAbE6NLo8YlGIxREIshIqVl+mQ6j8/zFVCb4UtuZua795577rl3FCEE5Bl79vPd5LHYiOP7cH1AUWi85ytmvlas1bJ9E5ryBntH3BpuP/X9i7ovkluuiE8N9SDepaLpCcRCCqa/VDCaMuIjSWP25Upl6n+QDoCz6Yh7KKzh3sI2LuUimPtRRyaqodj0MDloYiITSTi+mH29Wu0AUf9CsZPJoW5czJl48LmCc5kIKo5Al67B9gUGYxrun+5NnMlFZ+GKiQADj2a7AquseLIvjMv5KMaSBu4sWVir+3i8VIVKYSby0UTdFU8Znu8AYBHQgVOJEN5uOXi4UsdawwU0FSf6TaSoyw6DRvukPkgGWpDKy4F8a3jImCrqFDFn6rhKPR4VGnhvOTAY3WLcjifcQAsqRfhUc/Gq1MKNbBh9nIAMDjEppocxs9HCMktfGTCwP/oOBkUKNk/qF3pDYC6Ktk8RfWzyaaoKrqdDaBDwya8W1m0/CPCR3kFy7CcnmWQRUJqcRJFUKtTnPCeR71LwoeYF92CYyVnCFZpCTrRtCv5to2St8SOrKxiPqEEA4fkYT+mI0rdoeUiH1XZVuQPpsIKqw2QmfifTsnOABiWySlH9uU0Hh2MqjsZV5LtpPSoGeN9rKnhBX7ehoOSLIIPfnGONXGMMWN7xUfVldYDbjM3mrh5HCDgS17DhHgDQcIU+XbBxnDTn1x1UuQcJ9iv7l5Q5e1zLGri92EDJFnoAgHtcfr6wbbVXUqq193+0z97n3UJt1+d51n7aHwEGAAHXJoAuZNlzAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-dxf { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAo5JREFUeNpsU0trE1EYPfNMmtdoH2kDNmJbaVFcaBVFpAsREQpFwY0bu3HjQnTj1mVd+ANcuC3qQixmry6E0kWFVIQ+bKy2tbFJm3emyXTujGca+4DkwsedfLnn3POd77uS67rw1vC79ek7fZEzpu3AYUqS9tKQGZPLpa3VXP0uFCmJ/8t9OLC3q/uJbcs5bkIybvdHoMsSbLKENRmvU2WcNnTjRFD7ML1WGSPJHI6sA4KRWMAWVDPxLYex3iCmfpuIh1QsFSyMxQO4GvXHHwOJ6XWSyIck8v6HQsnjAxFc7vTj2VwBg4aG78VdBHQFCk+dbVcxMdwev9gTSEC455sIBOu2KLsoJFzqasP9vjCeDBlYqzn4VXXwarGKZN7Crd5QfLDT/7KpBM84c9fFUFjFp2wdk6smflRsKKqMa7EgfJJ3Ac2OKlit2pEmBTQfngdpnupoU7BUtRGiiTe7fXiRqmK+KuDn6TpvYogmBRJcrOwIJLIWxmM+dOsyLKryQAaJpjJ1/AxrGO3SqdZt7kKZJrzJWBg5piHENuY8vV6e0UOye1TyftvC5l+gZB8SHJTwpSx4q4JeTUKaxhXoR57h7Rn+3iFolJ3xvPhab6HgJG/pJ7jsNP4sUX+jZiCgEsWd/DjH5IrSYpBUAr0yHpzSoXKOP25a6OBhndh0zcX1qIYM2RIbu6i0KiHD5B/GTMHG03kTGpEL7H80wHFOWwhqDZ+SpkBOtCDYJDhZE4gRcKNbYynAqbCMbXpwpVPFbEng0aKJGbYzK1p4wIegLlcEPmdt+DjXbzcsxFlCynRwwVAwW6hjqeg0Zt521SYCWCJvbe0Un29UDx7Hgrs3IEitHXkw3jOv2fl92D8BBgAJeyqBh90ENQAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-eps { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmlJREFUeNp0U01vElEUPfMFCEVArdoSqEA0KV246UJdUJM2Lo2JK/9FjXu3utJqTNz4D9worrsQExbFpAFT0TYp0CZ8pIAiyMfMvBnvm2Foa9uX3Lw7c98979x77hNM0wRf7ufPsq7Z2SQYw2QJAkDxQalUZa3WI8hy3gmZr15bu+z8kILBkCeRCJi6bufKMji0NhwiCQR6iitdatTvQ5LyOLLEiWcYukm3m4Zhmbq1BX13FyoxuH7xAlbvpqKRK1fT0PWbRwEmDEyiy1QVg/V1GO02tO1tKLEY2PIy3KEAlmJRDLXb0TeZL+n9g4MHlLJ5HIBuYnSzXq+DlcsQLk/D9Hoh1WrIUjlPcpsYGQzS3LWoaBhvKeXWMQCDA1D9pt8PaXERUjwOjEZQFhZQp9L2yERiqYRCkPt/z58ogTGqHQLE1BLgUmC6XGD5AlipBIFKkbhanKHGYLBDqQ4ZED0OAbfLlo8OIxwGvhVgyTHlA3xkomjH/gegBgDURMv6faDbBZpN+/tHkUApkdTA/PwZAPxntwdUyjYA/+ZMqJHjLgM9iv/6zRt2GgMaIE21aVIjnSm0DGPfmhzyde0UAE2Dj+p7urKCPvkZku9eJILOSMUnkvVhIo7GYIB3xSKYdhoA1erXGVKXpvFxZwdBonnD68PQ7YEwM4O4xwMPxc8RYE87g4FIcz+kvfmnA0YzIJIy77/m0OCqsTkkCTysKPjJG3viLei63Gm3kCO6UWqcMejjxecMPmxsoFKtYop6UNirYL9Wtc5OHqzznIXHq1na7OfMJROcK8a6O7MjW7nfzZdrd7jzT4ABACh3NGsh3GcdAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-exe { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAo1JREFUeNp0k8tPE1EUxr+ZzvRJO62lUAQaKIQ0FVJFjBBdoIkrDDHuXJi4NnHtX+HCjW408Q/QmHTRaCRRohIJifgiiBICTQu29mHfnc7MHc+MlECKdxZz595zf+c737nD6boOYzxJLC6Nhwej7e/24HkO779s7G6mMjcEwfKZ21+/d+em+RbagaFev28qEpZwzKg3ZckqCPH1nfS8hScIdyhBe6JqTG3PfyTTeLrwFhvbKdy9/xi5QglXL0yGJsKDccZY7LDIAwWHpSferWBh+RN8ni4UylVER8MY6PHj0uSpUK0hxzfTmWsUtnoEwO3rer64jEyxim6/Hy67DXaHExvJX3jw7CX8XjfORUdDlOohhU4fAVjILCPbm9V1yIqK2FgYt+ZmsZcv4lH8Nb5upXD7+hVMjIRQa8qeDg8UTYPU5cTcxSk4nS709XTD53ZhpD+IYMAPj+TBz93fZiz5oHV4AP1fGdlyHZIkIZkrI7GyhnK9CZXy+Aig6p1+HQAY003AcF8AVtGGfLWG9XTO4MLZ5cL0WAixoT4zVmPHADSiMo3hzHA/xgeDWFjbNg8H3A7kKnX0koEcPdTu/ylgRGZgOjNv38zoSXC8BZJDRKOlwGEV0VJVGM0y4joAPO1spXbx6sNHeD1uRIYGUCxVSRlDt1fC8rfvcDnsmJ+dOaLgoAs6AVLZPJJ7WdhEkUyT8GJpBflSBcVKDTvpDBw2GzQqQT1OgaZqUOhtFQUTUKnVTVWNpgy51YLVKph7sqKYkA4A1ScEfT66vm5kC3+ofh6Xz59FQ5bpkvE4QW3M5Apoyorhl9ABIKnFgNdTOh2NkJG6WSf9eRBJtmFwLDJmriUzeaOkYvvcXwEGAIVNH6cDA1DkAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-flv { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmtJREFUeNpsUl1PE0EUPbssLYUCXdpaC9gWoSTgAyFigiRGY+KjvuuTr/4A44MP/gx/gMYfwIsan0RjIjGiJIZgSIGFIoXSD0t3Z3dnd70zpITazuZmJzP3nnvumaMEQQCx3jx69SV3a3KWMxetpSgKxP3m242Do43SQy2k/YRydvds67n8a63k+FRSn7l/bdg5tdsAuM3he/5weDC8vLdqPLgIIpba2niux52mg//DqlsYSg3iztO7mczN3DJ3+ByCLgCBH4hOFEF7cDpzPCRyOpaeLGXSc2PL3HbnW3XaRQCPEgWI2MsRVAVqrwbX9bHxbhOKpiJ/bzpDOr2k68V2BtRNzMtqDEqPejY/4zSGjb54BM0mQ8k4xsDoIMauXxnqYOD7PmwScP31d0SS/eAuh1lrolFpIBQNQw2pqJdqsAlIceB1AJCIkkE/FZskXDQVRXw6IYHiE0nBEcaPXSSvJnGwWkQXAE4acAhbxPMJpOdHweoMhc9b2F8zwKizbdlyPLVH7QLg+JKBYzoorxzjz3oRzUoToaEw9KyO8XQW5AE5jrFT6AbAYVVNxCZ0Ka3So+DSTAoDiej5ywTySbls1OEDobhFlMcXxrHw+AbINEjNXgb7y6BndLhk8cRkHHbD7g4gEhiJFxsdhrDqaamBaDKKerGGSKwPI9kR9EZCaNA5ubE7A5s8IFhsrxQkgJhZoa/06xC5xRz2v+3BOjFlbqcGlquxsondT9vY+2pAJdeZR6fI355CgQCN2A4O1w7gkQ7cdLUOAKdhV6uFSv3kd/n8mT68eC8dKWLnY4FsfeZQh7nVVt0/AQYAsf5g+SvepeQAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-gif { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmVJREFUeNp0U0tPE1EU/trplAqlL0laiw40xASByEJIZFGVnSvj1j+gWxNXJq7VrbrwF7h10cSNhMRHojEuACVBKmH6SJQyJeXRxzzv9dyZPiCtN5lMe8853znf953xcc4hztDzZ1+C6fQMHAfd4/MBFG+p6h/n4OAeAoGNToi/eOm+A50LKRaLh6amoty2vVpZdotNXccMEK3LwZxa2bsDSdrAqePv/mLM5tSdMwYBYqyvw9zdhUn/L59P4OGtG8qlZCoH254/DdCdQBCxqZu+ugqnWoW9swN5ehp2NotgIo6bGQWGtaS8+vQ5V9a0u5S+1gfABEilAqdUgm98HDwUQkDT8JXoPPq+BoM5kCYmFT9jryn1+hkAt7heBx8dhbSwACmTAUwTgdlZ/CVKJaLnI1GD8TikZiPSR8Gxib8chH95mZTxgwWHwH7+gFMswqcokIRbjMO2HDCnZ1VvArpjEmnKZc8+cZJJYGsLsMiZ8AgwEqaY6Mb6RQR33JFhGECzCRyfAFXNu9v+RVNRZWIMuDJNuYMAaDycUFGhCOgtuAtFVDA83G5A8TrFDw+F5QMAxAKJJxz2xnW3RPJGbm+rCyjotZetH4DGzaSSeDA3h4Zl4R0JOEZWTpIzF4n/m995bNdqZwB6m0gFft3Ak6vz+KYWwFsGlqIxXItEcDt1ARMEtKdVgZb+fwA0G2C2hXM0ZTZNRcSf0b1pmXi7uYnjI+Lfanm5fRQsK8BIxKcrK7i/uIgP+Tw+FlREqHN5fx/vyU4uHBE6UO4gDWqk/JFaLuMxcXeFk6TuJ90V0HOk1in7J8AAjmgkPfjU+isAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-h { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAbRJREFUeNqMUk1Lw0AQnf0woK0ttVqp0hwqVCl+UBERT94F7x78Cf4Uz179DT14F8WbYHtRkBYRLNqDtdaPZLObuLs1NGlXcWDJZGbey+x7QUEQgIqT07PL5WKhHL5H46J+22q22vsWpbWwdnR4oJ80LNiz2czGUjENhvj4ctIE4Wrj8XmPUlKL9nCYcOFzE9j1OKSTCdjdrtiLdr7KhVgzEvwW6krC92E6k4Kd9bJt57JV5vFK2KfRQRV+RAMkzxglYI1RaDy2dW1rpWRjQo5VGicYIorWVooFvQVCCAjG8Omw1MgG8AM0uSBUDSnCfk/IGCHwf3DCD/7UhOLBrFkDuep/hDUSSCv1iYo4rIfqGwmUSNJjfYbBcQKhZw0aBMA4B48LwBhBt/cON80HmM9NQ6fXg/Wlku4TwmNWDzaQqzHG+0PSKod5cH5Vh2RiAhYKc8DlV1UPSyuFMGygVlMg1/P6BC6DqXQK8jNZDXAYA1f21V34wMXYFaiyVw0rJyzLgs3VMkxOjGtix/V0XWChZ0cI2i/dzvXdfTd0Qf91BMPrhyNzgKfOmxaWypqaDXHfAgwAtCL8XOfF47gAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-hpp { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAehJREFUeNqEUk1v00AUHK/XKf1yZdESVRBXjRSRFqMQVBA5Ic5I3DnwE/gpnLnyG3LgXglx4UDDLZS0RWkDLiRxSusk9u6GXSembmLgWZbX7+2bnZl92mg0goo3b3ffO/ncdvyfjHef6q2Dlvs8Q2ktzr16+SL60jhhZ69bO8X8ClLC7w9XdKJVG8fuM0r1WrJG4gXjgqU1D0MGc2kBTytl+7a9XmWcl1IB/hZKEhccq5aJJ/e3bTu7Wg1CVo7rNLlRhUh4oMnXoDoyhoHGyWmUe+QUbELIa7W8CjAFlMzdzeckCwFN06ATAn8QmDMMMGlMuwWucpoCHNe4jBkAMenjYvRPTyi53JvuwX8AplleAeBcRFrH6rXIxLim9I/pi3QA1RhKaYxdjkN8IwalCMIwWs9ljMkh0wzk+9M7w179C3LZNXxve2h+c3Hu91HeKmD/6zHOLnw83ilB1/V0CeqU3Q81LC/O41b2Btx2N2JVP2riR8eTUxmi0TzBwrKZMsqMoz8MsDh/DWuWhUBKURLKxQIeOMWoptYPnS1c+INZBkwISomOSsmBZS7B+3WOzZvrKGzkMAiGqNy7g+LmRkRfekBnANy2163PZXrSbrQ6vch19Xz8fPDHyL39QzkHBKedXjfu+y3AAGU37INBJto1AAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-html { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmBJREFUeNqEUktPE1EU/mY605a+hhZTBNKRDApNrWIRA4nEBUZdmCgLNi4MK5f+FNdu3bFv1J1EXODCR1JJSMTwpqUP6NiCpe10Zjz3hj5Mm3iSybl37jnf+c53jmDbNpi9eb+6Ftcisea909bWNzNb6dwzSXKkhIt/r14+515qBqmDA8HpqKagh53XaopblpIbe+knDpFAhPab2Dw0TKvRK7lmNODzePBgZlK9oUWSpmVNdpIU8T+jaMsyMaD4MDcZVa+NhJMN00w0n6V2nN3yQgdHWZag+LzYPTomIAtT0THVtPGanmb/BbjwLFkvn2IttYGYplKyDzsHh7gdmyAWfh5zVq0Guhg4RAHFUhmfvq3j134aXo8bd+ITnMFOOovU5jbGRoZwNxFn1cxuAIcDW/sZDjA/c4u+BNxOJyxqaenpI3z88gMfPn9Hv98HQZS6RazW6kjExvFi8TGdDSy/W0Emf4LS6R8sv11BmfzSwkPcm74Jo9Ei0GZgmkw8QCOao8OXcaz/5vSZnPdnp3ApqBBLkWJE0Ci7ASzbIhCLLQ1E0iOkBDh9NpUgiUejo8oNuJwyn0YPABtn51UYFFivG3yBGCNZkuDtc/MW+ZQI3OrYpBaARCKufk3B5XIiWyhiL5ODp8+FfFHH+KiKSqWKUL8fC/NznGlPBmz+24dZjKnD0CJDcMoyW0SqXuMtHBFw7rhIAD1ErNUNafxKBNevapwu65NpEQ4FqXIA+RMd6VwBP3cPSERb6gLIFIq61+UqGWaFdcrVt/lmAuWjAi2aiMFwmOYuIJ/N6M28vwIMAMoNDyg4rcU9AAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-ics { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAhRJREFUeNqEUkFPE0EU/mZ2dra7bLNpi2AxQFKalkJrohICiYkXPagXrx78Df4K48GDBzmQePLMhUODNxQ5ciEkJVqDtJGmMWrCATRbd2ecoS5u3aovmezsvu9973vfPiKlhI4XL7c2r5YL81LIELEghLA3u/udxmHnPmfGW/Wuv+LpwwdneRYBx7PeWK0wOYYhcXxyckGV1fdbnbuMsXcklqPRJQxFMKz4RxDCtVO4s3xlRjWoB0FYjlQPEEBieChwKCRGMx5uLtaKs1P5ei8IKlGa/YkXMXYtlTEDlsnw/mMXhBJcqxSK6vlcpa4PEpCooUyIqs5M6hG1o2CUwqA091cFcYLf/sjzcX75EiQIojI9779CTYR4jwTBf+r7GAwh0AxCiL6JMT/04vQ79u8aI2O/7Jzg69o6Go8ewycUahtBpADhHKLnK/eVbkMdtROWIv80NQ2sPhncA9Htwn+9hZG0rY6DzFwJl+7dhs0ZstUy8rduwPS/wd/ehmi3kwq4zTHiWUgXp+EuL8FvNvFl5Rn4xAS86iyI2kY3n0Mv48ByrOQmancdi8I0Kcj3U5iuA29xAelKCUHrEIayzltagG2E4IwkFaQgSC6lYI09iN0d8It5uNV5nG5sgJdKYC0G8WoTOZvBISFNEBxnsuzD3GX4vfDsszzqAu0jkJQDedCGbB6AWg54pYbPo+NGVPdTgAEAqQq70PytIL0AAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-iso { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAjlJREFUeNp0kstrU0EUxr/k5qbJzdPYpGkpsUJoA2q1oLjTdiGiIC5cuXHlxv9BEOrStTvBnQvRrSAIsejCrlqpsURq2hCJNQ+TNLm5uc/x3MmzJh34mDNnvvnNzOE4GGOwx8+t9XQkfn0VE0Y5/7Z+kHm+dvOhtd3P9c/xwNZh7nWaMYtNUmX/Fct/vlN7/8J5aRRgyzm8xzpRDjGE2aVH4VTqdnoUYg/XkEhmy+Cx3DhA5tMzdFolvg5Mx3Fx9SmH0JIg79Zo3j4GADMIokJTKtjbfAKXU4Y/2NvSfyH75TFOxa9Cmr0XnlPFl5ReOQ6wNMDsoFX6AElqQlNV1KsOuNwS/AGFjEUIDhmn5+/DMM16/9igBowAzFKIswPJr6MjlxFP3sV04gaP7RzMPe6xvWM1gNUBM2UKYlBau3QghGphg29J3gDlLLilWNdD3gkvIIDRhD9yGe2mCV0V4HFXuCxT5Dlv8Dz3sIkAs03FalDxBMQSt9BRBMhNncuO7dyU28c9tnf8C/Q0ZtR4GImeQSj8APLRH772BWcgiFODffCv/t8H9tO0v3RjV7VqkeeXLlzDfvYjj88uXhl4JwIsrYxmLY/M1gYclIvGE9jZfNPrSCD3/QgLyeWTADV6wW9AryIcCkB0u1Aq/oCPumlufoF72vIheaLDr4wCLIOqrYnULA14PSoqpSJEAUilZrD77Sv3LK+cI0+Be8cAbbmAOrob0agtD491LYfkoqvnyZLsWRkA/gkwABL4S3L78XYyAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-java { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAjxJREFUeNp8U01v00AUnNiOEyepQyhQobRBSlVIoRCBEPTAjQsSEneE+An8FM5cuXLNoQduIAE3qopKNJAIIppA2jrOR93aa6/N8yZuUxyxkrXr3ffmzczbTQRBgHC83nj3ca28dD36nx6fvnzrNNrdp4oibyUmey9fPBezEgWVFuYLdyvlPGaMY4fl1aRS+9pqP5ElAkmcnknRwuO+Nyt5u/ETYfyj9WrpZnmpxn2/Ok1Swn/GvtnH5k4TLue4kNfxoFoprRQv1TzOb8cAIu3+ZD7oD/Hm7XuxzqRUNDtdkuLiTmW5tFxceBXlnXgQTAORSMt2oGezUJJJrK9dFWdEH7Ik4dB29LiESeUEJXd7/dAT3L+1ivlCHr8NEzutXTBvbJPPSdO/AH5wysChwM/1HzCGlmAzOrKxu2eCud6Z2Jke2MwThpUXL6Nn2ZAVFTlNw70bK0iRnGAq9qwHtOmTRpsx1NsHyKRVnNPnoMoK9kc2BjbD4vk5JGV5NkBoEPM4FFnCteJFWOS4ntHEfphQyKaFTWFLw704AJ26ZFx/ZEEi3YyY0O1Dmr4EKTUHA8hUnS6siI0DEHLYog+b28RCRuNXR/iQUpPUEQ+NVht6Lodnjx+GXYgDSFRnq97Ed2pXSlXhUSeGhxYc5sKlNXM5DGLR2TMwfZVPAIi+otGNWy1fEZUKeo4qc4ysI+F8VksLIJfYcD9QYgB/DNPMptWBlsnBIS86xmDMTBo/PWd0LB6VZfdEbJT3V4ABAA5HIzlv9dtdAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-jpeg, -.ipfs-jpg { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmlJREFUeNpsU8luUlEY/s4dmMpkWxRopGJNNbiwhk1tItbGtXHr0hcwmvgOdWld6Bu4coXumtREE3ZKu8FgOlC1kIoXtC3jPfdc/8PUIpzkBM7wf+f/hsts24YczuerGUc0moBlYTAYA+i8sbdXtAzjITRtq39kr73s/Gr9DTUYPOeamwvYnHdrdR0SnDebuCbswJGqpX+Uf92Hqm7hzFAG/4TgNr1uCwEJ0trcBC8U0Kb1/PQkHt9JxSLnL6TB+Y2zAIMOJBGLXmtsbEAYBsx8HnqCGKVScAX8uHf5EpqmGXv18VO6VDEe0PXsKABN8+AAgiabmYFNNJTDQ2RUFc8+Z9G0OPR4PKYwvKari0MAgiY/OQGCAajhMNR4nDZMaInrKBGl70SPMScck1NQG3X/CAWLE3/dAWV5hRRVIJxOWNksrP19sFgMqqAebUGYHMI6teq0A9oTVAhqu2sfbYYjsL7lCZ3683gA70T3TK7/B4BNoO020GwB9TpwfAz8LgMtWn/NkV8EHgoB81c7nYwCyBZlEVkHcqMTKFnkmehJTOPvEfCnKi0fAyADJKfXC/h83TaZTJjaa5lANLpOFqAXtlEAorAwO9u5syT5UxLfU0e3o1FMu1x4u7ODYq02BKAMAVSrSNLrK1MhLPj8mNF0vFm+C1ZvwKBwXXE4AGn1WAASazESwUW3BzUSMeJ2o1Aq4sPurvQYSRLwlhRR6mSaYyi0WlpAJrFRx3ouh5/lMt5lv8BLwXp0M4lSpYL17e2uK5wP6lj/c2ZPn2RI+YT8fDvqoyegVLyfG5kBKaQQOfvF2pLc+ifAABiQH3PEc1i/AAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-js { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RUQ5ODY5Q0NGMTE4MTFFMTlDRjlDN0VBQTY3QTk0MTEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RUQ5ODY5Q0RGMTE4MTFFMTlDRjlDN0VBQTY3QTk0MTEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpFRDk4NjlDQUYxMTgxMUUxOUNGOUM3RUFBNjdBOTQxMSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpFRDk4NjlDQkYxMTgxMUUxOUNGOUM3RUFBNjdBOTQxMSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PoT8zQ8AAAJdSURBVHjadFNbTxNREP52t7S0bktbKFAvTUVaw60YqkExUTD6oD74qC/yD/wp/gh885XEEI0RAyYQUiMpIBGMkYR6o23abi+73e2uc04v1LROMtnZPTPffvPNHMGyLDB7sbJ2ciUSli3U35smkK9t7x9v7n2dD/g8KUkUwWqeP3vKz23NxJGzgwOx0RC6mSgIo+WKuvP56MeUzy2nJEk8PWsGJVVTuhWbpgmHw47FB7d98Wg4mVWK52o1sxOg3Va3PmFp+Q2PdUquaFUM9/vw+O6cP3bxwm46Xwh1ALR3/vL1e+hGjcc9koScUsTSq3coVDQsXJ3wzo5HEs3clgZNMTVdx1T0Ep7cn6//QRQwMhzA6uZHLD5cIFEFSKIU+G8LK+tb0KsGZKcTJoEyP08AbpcLy6sbPKdQrigdAGaDwWxsDH1uGbliCYIgcM8WFPg8Mq5Pjzdyu4jYbCE44EepXMHuwXe+A8x3KKYxYsjvbUzmlPGpBmYdgI1oYjSMbL4Ao1YXMkcM2Dd2xnbAamPQAqg1GORLZdycmYTdJqFKk2DPR3fmwI4zBDrg9RADqxPAbPBif2WTSB584/3/TGegEOit+DRcvQ4OZJi1LgwIQKVCg2i6nb1I7H3Br3QWqT9pBAP9uDY5xjdSM3RqxeoUkfVnEOW8UkLykERTNXjkM7h3Iw6NNvHw6JjuhAhVrba0+QeALozcI9nQR0VvNxJc/ZmxCNGvIBQcpDG6udA22kyW29HC72wu8yG579ZoiSYuR/ly2+y9CA4NceWLmo717T1i5ULqJNtapL8CDACskxPFZRxLwQAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-key { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAlZJREFUeNpsU11PE0EUPbM7u/2AtJUWU6qiiSYYo5EmmPDCD9AH46sx8cEnja/+CB989z+Y+MKPgMiDsYQACcbaWBBogYD92t2Zud7ZlQZsbzKZ3bl3zj3n3IwgItjYeDO3MlWme0bjUth8e8/fO2tHzx3XqUEk50uft+Ndnhdmc3SlfNPkVZT8Cy600DoIISvVfKYtlvfX1p66XmoIYsMZdjJQWvEFbbsC/S5g2QhSkKUK7rx6OzvzqLpsovAhaAxA3DUBQn2TUFsl7KwTfm4Z9DoO5LW7uPXi9Wxpfn7ZKF09vyPxX2iWcNRkKGZz0mQWKoNs8AVB6x1yRY2pYnc2LLofuXTxMgAlmlXIfngCxNxEzM+DPv6NQa2BygLgZyX6JT83ngHTN5GAL0WSoUQkSQnXkyBh/k0GegTAaldM20sTKvet+yyhIZApECamL0jUSe3oFChx3TopM4TeEQP2gc6BgGIwb4KGNXRhCkMGxgg2kJeybRiZM45D8W61qEAknSmpHStBhywu0nFVupSCTAcM4ECwqapv+NQ6LS9JGALoMIIoPYDjZiEL1xHtbyO39AQUDaA7R1AH23DSeSA4hv5RG/VAhxomPYP8sw9A4TaC9iHkjUWmrtGvbyC18BLe3GP0m3WW4I5hEBEnPIStXzyuFIxb4EkMEJ79Qa/xHbKxCdM7xeCwzUZOjgEwnuzt7qLz6T3cySmQP43uzjeIiTJM6io6W19B/NLCKMVGCzkCoLR/0lrfOI2fNy/huKC1FTsK/rbGNeMRC8dHpHByfu+vAAMAL/0jvAVZQl0AAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-less { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RjZERjZENTJGMTE4MTFFMUIwOEVERjQ5MTZEMkVBREUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RjZERjZENTNGMTE4MTFFMUIwOEVERjQ5MTZEMkVBREUiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpGNkRGNkQ1MEYxMTgxMUUxQjA4RURGNDkxNkQyRUFERSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpGNkRGNkQ1MUYxMTgxMUUxQjA4RURGNDkxNkQyRUFERSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pl1w97IAAAJhSURBVHjahJNLbxJRFMf/wPAIMIxMkUI7tS0VYqlGDLGhjdKkqyZ24cJFN925de+XcONHaHRj4k7TND6SGo1VWwmp2kSLhlqMDbQ87gzPYcY7k4GgoJ6bmdw598zvnvM/95pUVYVma+svcovx8yMnFZHAMJPJBJfDzq5vpX6+/vD5qo/z7DOMBdo/d26t6jFMJ3iY51jBz4M+LP6wxEw40Gy23qYzB3HO7fpmpZCOmfEfa7Xb4NxOrC4lvbPToe2yKE3K1PdPwNOtHdx79ESfq4qKkijB5/XgevIyHxEC24USmewDqD2ABxubaLRkfW6zMqjWGlh7/ByyAtxYnOPnL0Q2+gGGmKRaw8zUBJaTiS5QOO1FJnuIAM8hciaIWHgi8NcSNt+loVDY8JBXh2ojJAR1HbTSNFMUpV8Dxcjg0nSYBrtBxdLbqI1iheCUh9XXNGurAwCdEkb9QyBSFam9TDfoPZ1LUg1BH28IiwEARTVAQOzcFKRaHZpLoa9avY6L1Gfs0c32t4PU6W2lWsV8LAorw0Cs1nXftYWE3qZGqwWHzYp2zzlgetuolVFvtiDLbRRKFTAWCxx2G/KlMtXFhWPqOzsWHJwBx7rxKv2R7mwFz3lw9/5DLC/M4Us2RwV0g3U58XJnF7dvrsBOoX0Abbej/DFKRMKI30fTVGC32WA2m5H9cQQvhYi0vE/7Wdgczn6ARA9QPBrBszcp/XvpyqxebzQ0Tlsq6llxLhe9bD4cFMr9XdjLHpLv+SLGBYHAYiVu1kNOpAaRTWbCejgiw0zGhFGSK1aw+zXbvfK/BBgAPwADAs5GpGsAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-logo { - background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 553 235.3'%3E%3Cdefs%3E%3C/defs%3E%3Cpath fill='%23ffffff' d='M239 63h17.8v105H239V63zm35.6 0h36.3c7.9 0 14.5.9 19.6 2.6s9.2 4.1 12.1 7.1a24.45 24.45 0 0 1 6.2 10.2 40.75 40.75 0 0 1 1.8 12.1 45.69 45.69 0 0 1-1.8 12.9 26.58 26.58 0 0 1-6.2 10.8 30.59 30.59 0 0 1-12.1 7.3c-5.1 1.8-11.5 2.7-19.3 2.7h-19.1V168h-17.5V63zm36.2 51a38.37 38.37 0 0 0 11.1-1.3 16.3 16.3 0 0 0 6.8-3.7 13.34 13.34 0 0 0 3.5-5.8 29.75 29.75 0 0 0 1-7.6 25.68 25.68 0 0 0-1-7.7 12 12 0 0 0-3.6-5.5 17.15 17.15 0 0 0-6.9-3.4 41.58 41.58 0 0 0-10.9-1.2h-18.5V114h18.5zm119.9-51v15.3h-49.2V108h46.3v15.4h-46.3V168h-17.8V63h67zm26.2 72.9c.8 6.9 3.3 11.9 7.4 15s10.4 4.7 18.6 4.7a32.61 32.61 0 0 0 10.1-1.3 20.52 20.52 0 0 0 6.6-3.5 12 12 0 0 0 3.5-5.2 19.08 19.08 0 0 0 1-6.4 16.14 16.14 0 0 0-.7-4.9 12.87 12.87 0 0 0-2.6-4.5 16.59 16.59 0 0 0-5.1-3.6 35 35 0 0 0-8.2-2.4l-13.4-2.5a89.76 89.76 0 0 1-14.1-3.7 33.51 33.51 0 0 1-10.4-5.8 22.28 22.28 0 0 1-6.3-8.8 34.1 34.1 0 0 1-2.1-12.7 26 26 0 0 1 11.3-22.4 36.35 36.35 0 0 1 12.6-5.6 65.89 65.89 0 0 1 15.8-1.8c7.2 0 13.3.8 18.2 2.5a34.46 34.46 0 0 1 11.9 6.5 28.21 28.21 0 0 1 6.9 9.3 42.1 42.1 0 0 1 3.2 11l-16.8 2.6c-1.4-5.9-3.7-10.2-7.1-13.1s-8.7-4.3-16.1-4.3a43.9 43.9 0 0 0-10.5 1.1 19.47 19.47 0 0 0-6.8 3.1 11.63 11.63 0 0 0-3.7 4.6 14.08 14.08 0 0 0-1.1 5.4c0 4.6 1.2 8 3.7 10.3s6.9 4 13.2 5.3l14.5 2.8c11.1 2.1 19.2 5.6 24.4 10.5s7.8 12.1 7.8 21.4a31.37 31.37 0 0 1-2.4 12.3 25.27 25.27 0 0 1-7.4 9.8 36.58 36.58 0 0 1-12.4 6.6 56 56 0 0 1-17.3 2.4c-13.4 0-24-2.8-31.6-8.5s-11.9-14.4-12.6-26.2h18z'/%3E%3Cpath fill='%23469ea2' d='M30.3 164l84 48.5 84-48.5V67l-84-48.5-84 48.5v97z'/%3E%3Cpath fill='%236acad1' d='M105.7 30.1l-61 35.2a18.19 18.19 0 0 1 0 3.3l60.9 35.2a14.55 14.55 0 0 1 17.3 0l60.9-35.2a18.19 18.19 0 0 1 0-3.3L123 30.1a14.55 14.55 0 0 1-17.3 0zm84 48.2l-61 35.6a14.73 14.73 0 0 1-8.6 15l.1 70a15.57 15.57 0 0 1 2.8 1.6l60.9-35.2a14.73 14.73 0 0 1 8.6-15V79.9a20 20 0 0 1-2.8-1.6zm-150.8.4a15.57 15.57 0 0 1-2.8 1.6v70.4a14.38 14.38 0 0 1 8.6 15l60.9 35.2a15.57 15.57 0 0 1 2.8-1.6v-70.4a14.38 14.38 0 0 1-8.6-15L38.9 78.7z'/%3E%3Cpath fill='%23469ea2' d='M114.3 29l75.1 43.4v86.7l-75.1 43.4-75.1-43.4V72.3L114.3 29m0-10.3l-84 48.5v97l84 48.5 84-48.5v-97l-84-48.5z'/%3E%3Cpath fill='%23469ea2' d='M114.9 132h-1.2A15.66 15.66 0 0 1 98 116.3v-1.2a15.66 15.66 0 0 1 15.7-15.7h1.2a15.66 15.66 0 0 1 15.7 15.7v1.2a15.66 15.66 0 0 1-15.7 15.7zm0 64.5h-1.2a15.65 15.65 0 0 0-13.7 8l14.3 8.2 14.3-8.2a15.65 15.65 0 0 0-13.7-8zm83.5-48.5h-.6a15.66 15.66 0 0 0-15.7 15.7v1.2a15.13 15.13 0 0 0 2 7.6l14.3-8.3V148zm-14.3-89a15.4 15.4 0 0 0-2 7.6v1.2a15.66 15.66 0 0 0 15.7 15.7h.6V67.2L184.1 59zm-69.8-40.3L100 26.9a15.73 15.73 0 0 0 13.7 8.1h1.2a15.65 15.65 0 0 0 13.7-8l-14.3-8.3zM44.6 58.9l-14.3 8.3v16.3h.6a15.66 15.66 0 0 0 15.7-15.7v-1.2a16.63 16.63 0 0 0-2-7.7zM30.9 148h-.6v16.2l14.3 8.3a15.4 15.4 0 0 0 2-7.6v-1.2A15.66 15.66 0 0 0 30.9 148z'/%3E%3Cpath fill='%23083b54' fill-opacity='0.15' d='M114.3 213.2v-97.1l-84-48.5v97.1z'/%3E%3Cpath fill='%23083b54' fill-opacity='0.05' d='M198.4 163.8v-97l-84 48.5v97.1z'/%3E%3C/svg%3E%0A"); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-mid { - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-mkv { - background-image:url("data:image/svg+xml;charset=utf8,%3Csvg id='Layer_2' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 100'%3E%3Cstyle/%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='36.2' y1='101' x2='36.2' y2='3.005' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23e2cde4'/%3E%3Cstop offset='.17' stop-color='%23e0cae2'/%3E%3Cstop offset='.313' stop-color='%23dbc0dd'/%3E%3Cstop offset='.447' stop-color='%23d2b1d4'/%3E%3Cstop offset='.575' stop-color='%23c79dc7'/%3E%3Cstop offset='.698' stop-color='%23ba84b9'/%3E%3Cstop offset='.819' stop-color='%23ab68a9'/%3E%3Cstop offset='.934' stop-color='%239c4598'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill='url(%23SVGID_1_)'/%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill-opacity='0' stroke='%23882383' stroke-width='2'/%3E%3Cpath d='M7.5 91.1V71.2h6.1l3.6 13.5 3.6-13.5h6.1V91h-3.8V75.4l-4 15.6h-3.9l-4-15.6V91H7.5zm23.5 0V71.2h4V80l8.2-8.8h5.4L41.1 79l8 12.1h-5.2l-5.5-9.3-3.4 3.3v6h-4zm25.2 0L49 71.3h4.4L58.5 86l4.9-14.7h4.3l-7.2 19.8h-4.3z' fill='%23fff'/%3E%3ClinearGradient id='SVGID_2_' gradientUnits='userSpaceOnUse' x1='18.2' y1='50.023' x2='18.2' y2='50.023' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3ClinearGradient id='SVGID_3_' gradientUnits='userSpaceOnUse' x1='11.511' y1='51.716' x2='65.211' y2='51.716' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3Cpath d='M64.3 55.5c-1.7-.2-3.4-.3-5.1-.3-7.3-.1-13.3 1.6-18.8 3.7S29.6 63.6 23.3 64c-3.4.2-7.3-.6-8.5-2.4-.8-1.3-.8-3.5-1-5.7-.6-5.7-1.6-11.7-2.4-17.3.8-.9 2.1-1.3 3.4-1.7.4 1.1.2 2.7.6 3.8 7.1.7 13.6-.4 20-1.5 6.3-1.1 12.4-2.2 19.4-2.6 3.4-.2 6.9-.2 10.3 0m-9.9 15.3c.5-.2 1.1-.3 1.9-.2.2-3.7.3-7.3.3-11.2-6.2.2-11.9.9-17 2.2.2 4 .4 7.8.3 12 4-1.1 7.7-2.5 12.6-2.7m2-12.1h1.1c.4-.4.2-1.2.2-1.9-1.5-.6-1.8 1-1.3 1.9zm3.9-.2h1.5V38h-1.3c0 .7-.4.9-.2 1.7zm4 0c.5-.1.8 0 1.1.2.4-.3.2-1.2.2-1.9h-1.3v1.7zm-11.5.3h.9c.4-.3.2-1.2.2-1.9-1.4-.4-1.6 1.2-1.1 1.9zm-4 .4c.7.2.8-.3 1.5-.2v-1.7c-1.5-.4-1.7.6-1.5 1.9zm-3.6-1.1c0 .6-.1 1.4.2 1.7.5.1.5-.4 1.1-.2-.2-.6.5-2-.4-1.9-.1.4-.8.1-.9.4zm-31.5.8c.4-.1 1.1.6 1.3 0-.5 0-.1-.8-.2-1.1-.7.2-1.3.3-1.1 1.1zm28.3-.4c-.3.3.2 1.1 0 1.9.6.2.6-.3 1.1-.2-.2-.6.5-2-.4-1.9-.1.3-.4.2-.7.2zm-3.5 2.8c.5-.1.9-.2 1.3-.4.2-.8-.4-.9-.2-1.7h-.9c-.3.3-.1 1.3-.2 2.1zm26.9-1.8c-2.1-.1-3.3-.2-5.5-.2-.5 3.4 0 7.8-.5 11.2 2.4 0 3.6.1 5.8.3M33.4 41.6c.5.2.1 1.2.2 1.7.5-.1 1.1-.2 1.5-.4.6-1.9-.9-2.4-1.7-1.3zm-4.7.6v1.9c.9.2 1.2-.2 1.9-.2-.1-.7.2-1.7-.2-2.1-.5.2-1.3.1-1.7.4zm-5.3.6c.3.5 0 1.6.4 2.1.7.1.8-.4 1.5-.2-.1-.7-.3-1.2-.2-2.1-.8-.2-.9.3-1.7.2zm-7.5 2H17c.2-.9-.4-1.2-.2-2.1-.4.1-1.2-.3-1.3.2.6.2-.1 1.7.4 1.9zm3.4 1c.1 4.1.9 9.3 1.4 13.7 8 .1 13.1-2.7 19.2-4.5-.5-3.9.1-8.7-.7-12.2-6.2 1.6-12.1 3.2-19.9 3zm.5-.8h1.1c.4-.5-.2-1.2 0-2.1h-1.5c.1.7.1 1.6.4 2.1zm-5.4 7.8c.2 0 .3.2.4.4-.4-.7-.7.5-.2.6.1-.2 0-.4.2-.4.3.5-.8.7-.2.8.7-.5 1.3-1.2 2.4-1.5-.1 1.5.4 2.4.4 3.8-.7.5-1.7.7-1.9 1.7 1.2.7 2.5 1.2 4.2 1.3-.7-4.9-1.1-8.8-1.6-13.7-2.2.3-4-.8-5.1-.9.9.8.6 2.5.8 3.6 0-.2 0-.4.2-.4-.1.7.1 1.7-.2 2.1.7.3.5-.2.4.9m44.6 3.2h1.1c.3-.3.2-1.1.2-1.7h-1.3v1.7zm-4-1.4v1.3c.4.4.7-.2 1.5 0v-1.5c-.6 0-1.2 0-1.5.2zm7.6 1.4h1.3v-1.5h-1.3c.1.5 0 1 0 1.5zm-11-1v1.3h1.1c.3-.3.4-1.7-.2-1.7-.1.4-.8.1-.9.4zm-3.6.4c.1.6-.3 1.7.4 1.7 0-.3.5-.2.9-.2-.2-.5.4-1.8-.4-1.7-.1.3-.6.2-.9.2zm-3.4 1v1.5c.7.2.6-.4 1.3-.2-.2-.5.4-1.8-.4-1.7-.1.3-.8.2-.9.4zM15 57c.7-.5 1.3-1.7.2-2.3-.7.4-.8 1.6-.2 2.3zm26.1-1.3c-.1.7.4.8.2 1.5.9 0 1.2-.6 1.1-1.7-.4-.5-.8.1-1.3.2zm-3 2.7c1 0 1.2-.8 1.1-1.9h-.9c-.3.4-.1 1.3-.2 1.9zm-3.6-.4v1.7c.6-.1 1.3-.2 1.5-.8-.6 0 .3-1.6-.6-1.3 0 .4-.7.1-.9.4zM16 60.8c-.4-.7-.2-2-1.3-1.9.2.7.2 2.7 1.3 1.9zm13.8-.9c.5 0 .1.9.2 1.3.8.1 1.2-.2 1.7-.4v-1.7c-.9-.1-1.6.1-1.9.8zm-4.7.6c0 .8-.1 1.7.4 1.9 0-.5.8-.1 1.1-.2.3-.3-.2-1.1 0-1.9-.7-.2-1 .1-1.5.2zM19 62.3v-1.7c-.5 0-.6-.4-1.3-.2-.1 1.1 0 2.1 1.3 1.9zm2.5.2h1.3c.2-.9-.3-1.1-.2-1.9h-1.3c-.1.9.2 1.2.2 1.9z' fill='url(%23SVGID_3_)'/%3E%3ClinearGradient id='SVGID_4_' gradientUnits='userSpaceOnUse' x1='45.269' y1='74.206' x2='58.769' y2='87.706' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23f9eff6'/%3E%3Cstop offset='.378' stop-color='%23f8edf5'/%3E%3Cstop offset='.515' stop-color='%23f3e6f1'/%3E%3Cstop offset='.612' stop-color='%23ecdbeb'/%3E%3Cstop offset='.69' stop-color='%23e3cce2'/%3E%3Cstop offset='.757' stop-color='%23d7b8d7'/%3E%3Cstop offset='.817' stop-color='%23caa1c9'/%3E%3Cstop offset='.871' stop-color='%23bc88bb'/%3E%3Cstop offset='.921' stop-color='%23ae6cab'/%3E%3Cstop offset='.965' stop-color='%239f4d9b'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill='url(%23SVGID_4_)'/%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill-opacity='0' stroke='%23882383' stroke-width='2' stroke-linejoin='bevel'/%3E%3C/svg%3E"); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-mov { - background-image:url("data:image/svg+xml;charset=utf8,%3Csvg id='Layer_2' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 100'%3E%3Cstyle/%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='36.2' y1='101' x2='36.2' y2='3.005' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23e2cde4'/%3E%3Cstop offset='.17' stop-color='%23e0cae2'/%3E%3Cstop offset='.313' stop-color='%23dbc0dd'/%3E%3Cstop offset='.447' stop-color='%23d2b1d4'/%3E%3Cstop offset='.575' stop-color='%23c79dc7'/%3E%3Cstop offset='.698' stop-color='%23ba84b9'/%3E%3Cstop offset='.819' stop-color='%23ab68a9'/%3E%3Cstop offset='.934' stop-color='%239c4598'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill='url(%23SVGID_1_)'/%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill-opacity='0' stroke='%23882383' stroke-width='2'/%3E%3Cpath d='M6.1 91.1V71.2h6.1l3.6 13.5 3.6-13.5h6.1V91h-3.8V75.4l-4 15.6h-3.9l-4-15.6V91H6.1zm22.6-9.8c0-2 .3-3.7.9-5.1.5-1 1.1-1.9 1.9-2.7.8-.8 1.7-1.4 2.6-1.8 1.2-.5 2.7-.8 4.3-.8 3 0 5.3.9 7.1 2.7 1.8 1.8 2.7 4.3 2.7 7.6 0 3.2-.9 5.7-2.6 7.5-1.8 1.8-4.1 2.7-7.1 2.7s-5.4-.9-7.1-2.7c-1.8-1.8-2.7-4.3-2.7-7.4zm4.1-.2c0 2.2.5 4 1.6 5.1 1 1.2 2.4 1.7 4 1.7s2.9-.6 4-1.7c1-1.2 1.6-2.9 1.6-5.2 0-2.3-.5-4-1.5-5.1-1-1.1-2.3-1.7-4-1.7s-3 .6-4 1.7c-1.1 1.2-1.7 3-1.7 5.2zm23.6 10l-7.2-19.8h4.4L58.7 86l4.9-14.7h4.3l-7.2 19.8h-4.3z' fill='%23fff'/%3E%3ClinearGradient id='SVGID_2_' gradientUnits='userSpaceOnUse' x1='18.2' y1='50.023' x2='18.2' y2='50.023' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3ClinearGradient id='SVGID_3_' gradientUnits='userSpaceOnUse' x1='11.511' y1='51.716' x2='65.211' y2='51.716' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3Cpath d='M64.3 55.5c-1.7-.2-3.4-.3-5.1-.3-7.3-.1-13.3 1.6-18.8 3.7S29.6 63.6 23.3 64c-3.4.2-7.3-.6-8.5-2.4-.8-1.3-.8-3.5-1-5.7-.6-5.7-1.6-11.7-2.4-17.3.8-.9 2.1-1.3 3.4-1.7.4 1.1.2 2.7.6 3.8 7.1.7 13.6-.4 20-1.5 6.3-1.1 12.4-2.2 19.4-2.6 3.4-.2 6.9-.2 10.3 0m-9.9 15.3c.5-.2 1.1-.3 1.9-.2.2-3.7.3-7.3.3-11.2-6.2.2-11.9.9-17 2.2.2 4 .4 7.8.3 12 4-1.1 7.7-2.5 12.6-2.7m2-12.1h1.1c.4-.4.2-1.2.2-1.9-1.5-.6-1.8 1-1.3 1.9zm3.9-.2h1.5V38h-1.3c0 .7-.4.9-.2 1.7zm4 0c.5-.1.8 0 1.1.2.4-.3.2-1.2.2-1.9h-1.3v1.7zm-11.5.3h.9c.4-.3.2-1.2.2-1.9-1.4-.4-1.6 1.2-1.1 1.9zm-4 .4c.7.2.8-.3 1.5-.2v-1.7c-1.5-.4-1.7.6-1.5 1.9zm-3.6-1.1c0 .6-.1 1.4.2 1.7.5.1.5-.4 1.1-.2-.2-.6.5-2-.4-1.9-.1.4-.8.1-.9.4zm-31.5.8c.4-.1 1.1.6 1.3 0-.5 0-.1-.8-.2-1.1-.7.2-1.3.3-1.1 1.1zm28.3-.4c-.3.3.2 1.1 0 1.9.6.2.6-.3 1.1-.2-.2-.6.5-2-.4-1.9-.1.3-.4.2-.7.2zm-3.5 2.8c.5-.1.9-.2 1.3-.4.2-.8-.4-.9-.2-1.7h-.9c-.3.3-.1 1.3-.2 2.1zm26.9-1.8c-2.1-.1-3.3-.2-5.5-.2-.5 3.4 0 7.8-.5 11.2 2.4 0 3.6.1 5.8.3M33.4 41.6c.5.2.1 1.2.2 1.7.5-.1 1.1-.2 1.5-.4.6-1.9-.9-2.4-1.7-1.3zm-4.7.6v1.9c.9.2 1.2-.2 1.9-.2-.1-.7.2-1.7-.2-2.1-.5.2-1.3.1-1.7.4zm-5.3.6c.3.5 0 1.6.4 2.1.7.1.8-.4 1.5-.2-.1-.7-.3-1.2-.2-2.1-.8-.2-.9.3-1.7.2zm-7.5 2H17c.2-.9-.4-1.2-.2-2.1-.4.1-1.2-.3-1.3.2.6.2-.1 1.7.4 1.9zm3.4 1c.1 4.1.9 9.3 1.4 13.7 8 .1 13.1-2.7 19.2-4.5-.5-3.9.1-8.7-.7-12.2-6.2 1.6-12.1 3.2-19.9 3zm.5-.8h1.1c.4-.5-.2-1.2 0-2.1h-1.5c.1.7.1 1.6.4 2.1zm-5.4 7.8c.2 0 .3.2.4.4-.4-.7-.7.5-.2.6.1-.2 0-.4.2-.4.3.5-.8.7-.2.8.7-.5 1.3-1.2 2.4-1.5-.1 1.5.4 2.4.4 3.8-.7.5-1.7.7-1.9 1.7 1.2.7 2.5 1.2 4.2 1.3-.7-4.9-1.1-8.8-1.6-13.7-2.2.3-4-.8-5.1-.9.9.8.6 2.5.8 3.6 0-.2 0-.4.2-.4-.1.7.1 1.7-.2 2.1.7.3.5-.2.4.9m44.6 3.2h1.1c.3-.3.2-1.1.2-1.7h-1.3v1.7zm-4-1.4v1.3c.4.4.7-.2 1.5 0v-1.5c-.6 0-1.2 0-1.5.2zm7.6 1.4h1.3v-1.5h-1.3c.1.5 0 1 0 1.5zm-11-1v1.3h1.1c.3-.3.4-1.7-.2-1.7-.1.4-.8.1-.9.4zm-3.6.4c.1.6-.3 1.7.4 1.7 0-.3.5-.2.9-.2-.2-.5.4-1.8-.4-1.7-.1.3-.6.2-.9.2zm-3.4 1v1.5c.7.2.6-.4 1.3-.2-.2-.5.4-1.8-.4-1.7-.1.3-.8.2-.9.4zM15 57c.7-.5 1.3-1.7.2-2.3-.7.4-.8 1.6-.2 2.3zm26.1-1.3c-.1.7.4.8.2 1.5.9 0 1.2-.6 1.1-1.7-.4-.5-.8.1-1.3.2zm-3 2.7c1 0 1.2-.8 1.1-1.9h-.9c-.3.4-.1 1.3-.2 1.9zm-3.6-.4v1.7c.6-.1 1.3-.2 1.5-.8-.6 0 .3-1.6-.6-1.3 0 .4-.7.1-.9.4zM16 60.8c-.4-.7-.2-2-1.3-1.9.2.7.2 2.7 1.3 1.9zm13.8-.9c.5 0 .1.9.2 1.3.8.1 1.2-.2 1.7-.4v-1.7c-.9-.1-1.6.1-1.9.8zm-4.7.6c0 .8-.1 1.7.4 1.9 0-.5.8-.1 1.1-.2.3-.3-.2-1.1 0-1.9-.7-.2-1 .1-1.5.2zM19 62.3v-1.7c-.5 0-.6-.4-1.3-.2-.1 1.1 0 2.1 1.3 1.9zm2.5.2h1.3c.2-.9-.3-1.1-.2-1.9h-1.3c-.1.9.2 1.2.2 1.9z' fill='url(%23SVGID_3_)'/%3E%3ClinearGradient id='SVGID_4_' gradientUnits='userSpaceOnUse' x1='45.269' y1='74.206' x2='58.769' y2='87.706' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23f9eff6'/%3E%3Cstop offset='.378' stop-color='%23f8edf5'/%3E%3Cstop offset='.515' stop-color='%23f3e6f1'/%3E%3Cstop offset='.612' stop-color='%23ecdbeb'/%3E%3Cstop offset='.69' stop-color='%23e3cce2'/%3E%3Cstop offset='.757' stop-color='%23d7b8d7'/%3E%3Cstop offset='.817' stop-color='%23caa1c9'/%3E%3Cstop offset='.871' stop-color='%23bc88bb'/%3E%3Cstop offset='.921' stop-color='%23ae6cab'/%3E%3Cstop offset='.965' stop-color='%239f4d9b'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill='url(%23SVGID_4_)'/%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill-opacity='0' stroke='%23882383' stroke-width='2' stroke-linejoin='bevel'/%3E%3C/svg%3E"); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-mp3 { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnxJREFUeNp0U89PE0EU/ra7XWxpSsFYIbVQf9REFBHkYBRIPJh4wrN3DsZ4MPGP8b/wUCIHEw5EY0w04o9ILcREGmwVgaXbbXdnd2bXNxPahGyczebtzrz3ve99740WRRHkWn5cebu4cH6SMY7e0jRAHr9c3WxsVvcemmbys9yT6+uHJ8oaPefypdPDD5Ymh5w26wMkEho8JtDtuEOZFCrvN/4uJZNGH0T59D58X/C27aFNAL3Xthmsww5GCyN4+uzu+OLtQsUPxPQx6ZMAoQjBAw7O+bEVCMMQgqygs+LFs1h+dGd8bna0QmXO9OL6JYgwAvOFZKKoy3V44CgNfv7Yx8oLH+lUEgvzF8Ydhz+n41snAGRG5gUEwClzhHdvttFxfNyYK0EnJozKK5eGcf1qHo1GOxtjwI+pfvm4g/W1qtJgerYE2SXJSIL9+W0jk0mCShAxDXgQKgbNXxZq35vQKCiKQkSUXdc1+gcch1FHGPmKuIgBCdc66qJQHMG9+1NIpUylxxHtuW6gEiTIu+N4yjdWgty0yTmdNjFzcwKjY0MU7MLt+IjoSad16FoIx3b/A0DZ7FYXnsdpAjUMDOjI5zPgfoBsRodhhGhZHfBBU/nGAGRtxWIOg5lT2NtrI5dL0SB5KJzLodloqXaOEatPGztKq5gG3S5DNjuAK5NjKJfPYKI0okBkSdemCiSgS/rkQNLSePtxBj4LSCwfFtE0krqqX7ZVMnu9XlMXy2l7ME0dzA3iANQyY6vWxC61UY41zTyNcYh6/QCNXQvzi5dR39nHVq1BUyuMGAARsF6tbbe4iKD1r7Om5iFBdmW1SsDflLiuB6sX90+AAQDHAW7dW0YnzgAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-mp4 { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnBJREFUeNpsk99r01AUx79psrTrujVtbceabnZs4DYRHSoMh6Dgq77rn+AfoA/+If4Bok+C0CfxVRDBh+I2NqZzrpS1DVvbtU3SJPcm8SSlsJlecsn9dT73nO85V/B9H0H78OLdt/LDlQ1uMYybIAgI9n99OWxoe83nkiz9hDDae330JvxL48O51Xxm/enNtKPbVwAh0Ec6kYpXat9Pnl2GBC02HrjM5Y7h4P8+7FtIFVJ49OrxUnl7ucIdfhv+BIDv+fBcj7p/tXMPrs2RXVTw4OX2UnFTrXCbbY7tpMsA13FDSDAOQ4gJEGUJLs0PPh9CkESsPrmxxEz2lra3rnpAt3G6adgdQhBpmeLkFodNmsjpOPoXBrQTDcmFFNS7i3MRDzzPCw/vva8ikU+COQxm14BBhvJcHLGpGPTOAJxxeLbrRgAkYujBdH4G5oWJWXUW19YL4XqunAMFhnq1BqWYgaY1MAHASQOiU96zKzkU76mwehaOvx6h9uMv7KFN3RopL4oTAI4HRh4wSl399xla+00YbR3yrIzM9SzSqgJJnoKcklGrH08CcJjnBtLLCsSEGGpSWJvHtDKNoFippsJ0ulIsDDUCCATMlBQkNuahEyiZTcLsmFBKaQxaOk53TlHeKkM70AjAooCghBOk9sKtIvqtPqS4FBaRnJSRX8tj2DOh3lFB5Qw2ZNFK5LRo6w4sKt2ggAzywidAMN/9uIPSZglBLDO5FF3mRD3wHE9qVRvoHrUpfn+UEQK0/7ShtwboHJ6jdH8RZxSC57hSVETb7e5/2u0FxqPHJow+8iZ4lYY2QGu3idhIxO7Y7p8AAwALCGZKEPBGCgAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-mpg { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnxJREFUeNpsU0tPE1EU/ubRdlqmnUBboa0UeUQDiUGCC1+JmrhxoXt/gBvXJi74If4AV0Y3sNKF0YUaICqoIfjgVShEiGF4tDOdO/fOeOaSKtie5GZu7pzz3e/c7ztKGIaI4vn9p+/P3h4e4a6Pv6EoQBDiy7P5rc1P1Xt6XP8M5ejXo6UJ+dWbuemeTGdpvNdiNe9YvQLe4Bi4PmTpRmyq8m71rp74BxKF2twIHvAo+f/l1T2Yp0zceHizfOZa/xRnfBRhG4CQqAYioBWeXDyA8Di6ei1ceXC1XBwrTXHPH2vW6ccBBBMI6BsSUEQzakGL6xB0tvjyBxRNxdCtc2Xf8R9TyaWTDOg2TjfVdw6hqIoE9B2GxkEDWlLH7s4ette2kSp0oDRezrQwCIIA3oGHr0/mKMmE53qo23W4+w5S+Q5ohob9X3tgHgO8ULQACC7gMx9mKQP30EW6mEHpYi8xcJEdzMucjfkKcrTfmqmiFYBxCF/Id+gayKJwoQjHdrA5v4HK7Cq44KjZNWpagaqp7QACks0H9znW365ia24DzoEDozOJbH8eVtGShXHTwNracnsG7q6LzsEuaAlNPm9h7DSSVjLyCMkppDI+GS2StQWA1RlKo0X56n2X+6QHkmkDakxF9WMVqWyK+s/BrthYfvWz1Ug+zUDcjMPMm0h3pxEjFma3CbIuCud7oMc0LL1ZgmElpGJtW3B+15HIGNITrMYIlOH7i0U41NrInREylYbu4R5qQbQBaAh95fVKZCnpQCnb9DrWZyrRERS6NDeUw+yHaXh7rt4C4B8y+9vkwn7kwKNRpDoa9aiFKBYnF+RcREqQ2e1m3R8BBgAy9kz9ysCE6QAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-odf { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAi5JREFUeNp0UktrU0EU/mbu3FfE1KRRUpWYheALNBURUVy7cy9UkO6KW/+Lbt0IPsFui4gLBbUqFaUuXETUKCYa0jS5yZ2ZO557b5MmTXpgmDPnfOc7jznMGINYPi0de5UvmpORxpjE/kbNqW005DVu8TWw1H758ZfkFgNgJmtyxSPRjJIj0QTW/RDiYGXGb7Dl32/eXrVsd0gSCx9miqC0ooCdp69g5Q/h6OLN0ty5ynIkwzMwUwh2FwMdcbDiCZQXlkqFCpEoPT/wih1YjLInANcD+/Ua9bu3wJlGvrBZCmet2+S6ME5g4oGlZ9A/I70XCDhhDexPNTFmswJBwcnuXkF86VSNZxVu0ukLSGnBcqlnN4HoCQIaIuIv7LUooMOgQ7q75LAAb59B9gCBHSKgqemRr94mMKmD24CfM8nb7THYGQNLpAkUkcb66JyGBFFEWRVL57gFEH5qj8Lxwca2qS3EZaugmzAw24dR/XQgwtsCSBjPIdWbUoE2UJLBnV8Ac/ciWHsK9/glWLnD6K2vgPszsOdOQdfeQ1c/ThKoTgDn9A3KUED/52d45xchZsvorD6Bf/Z60riV3Q9Z/0bbGU1uopYGkfERSQ3VbsMwl0qlqoIARmSoPYXWy0dor79LfBMEEd8jGs/uQ3Yl7PJFNFbuEXiV2riCf88fovXhBbo/vqP3t02/ZYmJFqTkzY160Go9uEMbFK8hR/NrdXtFuUVmnmySVGgO4v4LMAAjRgmO+SJJiQAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-ods { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAetJREFUeNqMUj1IHEEU/i7u7Z23e8tGgneGQPw3hZDkkhQiSuwMQREba4uUgpVlCrvEQhurkCoWqcQQ0oTAaYKNqJygGEwgHCSB6Knn7eXcdX/GmdHVPWYFP3gw78173/vmvYkQQsAwNvckq96UnyIEh7/d4t7uUd/8y+85P+bXSX4grkhI6nJYPW7LrXpBK2YxiSoShhu4Buq1NPofDeqdrZ3Z4cl7D4J3UtA5VyVAlmJoru9Af2ZAp1lcCQ3nqgiuKmbY3l/BH+MnHM9GVLP0Ww3KNA33CQoQQnL834Fj74PUGkANEIkCSSsa8gQqgYTIcB0PVsXB318GInRiCVWCkpRFAs+j5gKlA4t29Ggh4d0t04FKt9PQqF4UFgumSEA8ApeaElilWbYRVy/lsns/N1QBkxtENF4jxPxcgcB1CZVOrvMteK5IQDtJJIGh++PcX9iYwWjXK37+vP0WdYk0Ht99jtX8JywWFkQChw4tc+cZcvlF7rMze+ubbxN40fMalRMDP/6twaiUeK7wlZ0TD0a5hLTWxo2d45KKprqHKJslTsy209s2wnMFBTYNZjc/oLt9gPvLOx+hxVJIKS2YW5pCbSyJTGMK775O8VyBwDJd2LTDl/X5i8v3S7NVw9vJb51tITDEUwEGANCx2/rXEEFFAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-odt { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAepJREFUeNqMkz1II1EQx/+7Ca6JkqyYiJ8cKEpAQbBQFDm0sVOsFBS9wt5KOTgEG5twxVlZ+XEnKNiIghYKxx5nwEpIIXaiSAgKGmMi0d23u8+3T7OaZJEMLG9mmPnN/w1vBUopLPNNhRWXHOyDg0nx82TiJtZPlPVoNpftc2cTotcHtxx06kdXpSQ/BvzKESZzIDmAz6y+NojOjpDMZiqRPIgNoFyWM8DrKUV7axO+gcp4g7AzmquAdVNqOgL2z2I4id1B0wgeygOyt/rLL5buLwAIDgA9dY+L+DkuDQOCrkMgBsRglcMOqAGwIstMg8AkGsuZMNUMRMkLqE+QGloglvlA7uIOAKvZajR0qJkUj/XHe0BTIclVKKlrfKsj9qA8gA6wqSJzPaXlr7ky//tdLEUfawsBjExUFGVWbT7AxSa42H2LMfODmvd3wKb7RAMLYwM8nts8xJ/pEe7/3PmP2eGv3D+9usb35W0bINoA7RmjXSHsH0f5Z/mUSZ0Ir2JmsBtD80s8/rGyzWsLFTD5yUQCbfUBHl9d38LvkdDTXIuHVBo0k+bbt06qO+yAPGXwe/cA4wO9PN44jKDG70GougIzi2tQ00ms7/3lpwnBBgjZ37Kkd1Shht5XzBIFl/ufFtniT/lFgAEAU//g6kvdGBMAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-otp { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAcJJREFUeNqMkssvA1EUxr+ZjkdbrfFKVD12ErYSRELY2fkH+BMsLcQaSwsrSzZi47EjJEQkEhYkFlhYSVtFpdqOqpk717l3jKZmiC+5mZlzv/s795wzCuccQncz3YeRBj4KHz0/RrOZe2NsZPP20o255zQ3EAxzEAC+6uzTw13G4TFQAakA/CWtIYbY0KBOrx7IvwDQqlHV1o3YxKTOvyAUvfQCfqmA3e4ikyS/zRAKvOot7eoSHEgZIHrCfQAfBqBaKQQDKScQAExd8emBANg+2U2CvNMkkgSqBmrCxFB8mujeoJBWwEqARcssKTAJEGrmaGrjqK1zvNknH4BtyxKl2VUpRxmj5W+x73q9AEaZrR/ND1EJluIpS3i9JQiA+a+hSq8HwJjTsLrRaWitPTCOlhEZn5N75sM1qigmlN+dB3u++Qao5W4TtbEXXIsiszGL4PA00itTsu6XnQWo0TjMTAJqfMDx/ryBJcaVzSNSH4fW0Q+rkIf5rsjRiid7yyN7uoXS3Zn0egE0NiORAN9bQ017D1Lri7CLlP2EDr3Rf7C/itzV2bfXA/igLDaRixfngFhSCooH2xVPCWBlwKcAAwBX1suA6te+hAAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-ots { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAfZJREFUeNqMUk1rE1EUPS8zmabJdDKB2glEwY9ExJYiBUEQpV25qgtBXfgbpEtXuujKf+AfEKRddOdOGHClbYVCvyKWaijT2mhjphk7Sd7Me76ZONp0EsiBYWbOvfe88+69hHOOAE9f3zTVnDKNHvhlsfqPw/rM0ovyWsRFdXJEpDIyRnSlVz0KSkmvabaJeXSJBEhgAJzTDNybmtUnS5Pmg/lrN07H5NM/f13FoMgpXDSuhiIiK3Qi6LUugX7FAbaPPsJqfIHHKCStqRsXVFPQuZgD9BBxjikSiRq41AAkgCQBzVf0+BWEBX7GBm0xgHHUqk1UbBuEcIydzyCZlOI9YEGuDxwduCCitS3Xh3viCZ4jrcq4PJ6DLHd67tjtuAAXib54dCPVEfQ5XIcik/0/2iDeOYz3ceCxrisMi904y0XiMQFfkB7lg6xFHwFxEqUMV0anUNBLWKm8xd3i4zBWOzmASx0UsiW831mA59Xjm+h7HCOygduXHqJatzA7Poey9QnXjTuoVD/j/sRcmDOWLgqnLC5A2wwST+Pn8T629lahSCo291bwu9XA7vcy3m2+gTaUR14thrk9BXasbdiOjSe3nmPpwys0xSi/HpbDd3bIQC6dx/q3ZbRb/j8BEi3Po5cTJpHI9CBNDEa++GyDBN9/BBgAwfDlCVUQaNAAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-ott { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAdFJREFUeNqMU89r02AYfpJ0iVm7EqhVOxw7dDBEdpiCE1RoEZRddvUgbIex/Rs7eehppyF4LOzQu4MxwYp0HgShIuwwUVSCVtl0s13afl+SzzcpyZYmyF74eN583/s+PO+PSEIIeJZdrtQVI19Cgmk/Ph39bpllXq82g7sgLxVcyKNZpIx8Uj5u5zSjc9Gov8ZihCRC8D+7On4JczevGeTGSEIC4ctKJtB1DTPXi1iCCEkIm1EFlC2Em0iwtWfinXkIzjiO0jljtDC5TtflGIGUQMB+mfja/oPv2Rx9MMjpMdJxOXyXTwkcwIkewfqQ1QtQNB385zcI14FrtQexsSb6SRysZ4Fbf+F6eHwATc9gJGNAm5iCTL5n/LCVRGADNoeaGoHqyaXj5gqQlTODovcwNk5Aj6wXqV8eCo7EDhMonEHpW+dZC7gUG98D3geo7vkb01h9cAvPdt76OGy1xntUd3bjUxAk3+l2sHJ/FgtrT0MUJNfDSm0bjQ/72Hzxxo+NK+h3B7XRNO4UrwymQtMIkdTBU0m+sBOayLsn8Ka78mQDjx/e87HXPkb1+UsfP37+AmZ1fP/suknBb6nefVQXjl06TxMlJfWKNWr+Kv8TYAAkUueexJF47QAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-pdf { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmhJREFUeNp0U0trU0EYPTP35qYxaW6TlDapNKWGbgo2FkF8rARB6rboXusf0F/hyq2U4krFqugqSBeuAyL4SERBstHa0iR9JKZJ7mvu+M0tqZGkH3x8987jzDnnm2FSSqh4ns0VU1ybFzj674Wa3uWiWbfsFQb+jrGj8Xvbm0HlvYVRxhJprpmTlGmum+OMm5uNPZNbtjk3l82ey8++8oW4Jv/H/wdA456g2kvH99FyHNiuAz2dwflbN8YW8zMK5Go/CMfQkAhpGsyQgRCtlpE4jIULyC9fHzu7MPPEl/5ib6WOE0JJNRiHHg6j86mMjw/2gG4bkbY4PW4Yj2j64skA5FTHdaEMPiAJszt1sK0d4suJmY4k0+IDDGRfqmh0u5gejQc+fG8eYCIahRQCEfgQnIuhEkgtONE+dGxYxEDj1DhiEycZ+1YXdUpHCqTMJIYyEES5aXXQsi2kYlGEia5GtHVKn+amPBeCutPgfLALPuVu+xDVPw2EQyFEjHDghbpYNm1yKVVnYjTOerepn4E6XQmLGSPkPkOXWATMSDcjQEkAaqOu6+i/rccALtFL53LI3r0Nq1ZD4/MXZJaWYFer+PXiJc6s3IEgY3+uPYZHTAcAHM+DTE8gnM1CSyaCulv+GrRy8uYyElcu4XfhLVpkpNtn/DGA5Uu0abFH36WnzzCayWAkmYJvWeCkfb9SwY+NDbSoOx4bYqJF8rZqVRRXV/HhzWtUSmWwmWl0RmN4v76OUqGASrmMOkntSHF8MOs954dT08W248wzYsJDOujRBAaqqikTpRo/qqd0/dv97c3Lat9fAQYA4z8bX9nTsb8AAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-php { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAhNJREFUeNqMkltrE0EUx//ZbDaXNrvZzdIkbYOXGgxYQlCK2IIY6EufxGdB8Av44AdR8AP44JOPBR+Ego0PClUKTTXQSmkTYtOkmubSJrQ1e3H2yJSEJNIDs3PmP+f89pyZcdm2DcdWvn7LzkxFHmCIra7nm9ulg8yLZ09yXON55Dgjt1PM2iPs0+aW/frdh8bzV2/SvQBnCLiEqcFxLKSSodlrU9leiGPihWePBkgeEZO6ShC2dCAZNuf6ADb+ldQ5PUPx4BCFcgXfdwq4Ph1Dtd5CZi4Nw7SQiMdCXkl6yVIy/QBWgcU+yx/XsLK2cdHndqlK/lZxH/OpJO7fnsWY3z/YAq+g0TmHpoUH2vB5PXi8RD9Fo10aAmDJTgWyIuOupmK38rsPcOvqJO33XWEvwLJsmKxHRVEwf/MKWl/yUMf8mIloWN8rw+sP0D6PHQmYuzGNgCRiMZVA17IQV4OIaTI8buH/AJMFd02Tkp05PO4jnWvc57EDAINt7u1X8Pb9KgI+Lxbv3cFR8xjx6AQ+b+Txs/qL9KePlih2CMBCq92hg2qzt1AoV7H5YxdhdqhHzRbgcpFeqdUplpvQW4FhmAixZ/sws4BoWCM/qmsE5XqE3dDQCrqGAYWdejqZgK6GUD8+IV9VghBFN1RZJv3sT5diBwC15gncggCPJKF0WCPN8dun55jQdVpz3Ynl9leAAQAJhiGatD9AOgAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-png { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmtJREFUeNpsU9tOE1EUXXPp0CAUWmJbC04xBANNTF+kKhG8fID6aqL/gPEj9E0lIf6Dj30HL03wxQtVIC0QKrWxNG1Dk9Z2Oj1zxn1m0oIZTnIyZ8/ee+211z5Hsm0bYg29fLGpxWIJWBYGS5IA8ncKhT9Wvf4Yqprtu+w3q85X7f9QxseD/pmZMZsxN9fnc5JNw0ACGGv6tPSvyvEDKEoWZ5Y8OHHObKpucw4B0t3agnl4CJPs2YkQVu4s61ORaBqMJc8CDBiIRhhVM9bXYdVqYAcH8M3NgS0tQQsFcfdKHEbvlr6WyaR/V6uPKPy7B4DT7lUq4MUipMlJ2MPDUKtVfKZ2nn/5BoNbkONxXeb8LYXe/A9AJLNWCxgdhZJagDI9DZg9qIkEytRSkdqTSFQtGILSbgc8LViM+tc0yPfukzIyOJ359k9YR0eQdB2KmBbpwXoM3Dod1SkD+scpEapCI5DdpsJhIJcjajQZagcjI+5oLe4VkeQnyiZgdIH2X6BJ7dSqQLfrggjw0AQwP+/GegCIHppNoFAgEMO1RZKo7BQgRi3yN05cnwdA0BQMAgF3C6pnbuNg92M9AFT1diSCh6kb+FGvo2MxnBB9ocZxp4Mns1cde213B81e7xwAcl4jkaa0IUSjUdLJwkL0Ej6VSvArCt7l81iku6GrKnYEU89VJlSJRmR0Dax+fI9suYxSo4HlWIw6M3FBlnD9YhiXabyOsOeIqG7TzDeIYo6EDGp+ZPb2kKKqH8h+mkxiI5/D1/19J3bwYPvPWXq2skkiJVxesqt0XzghpKM8nRVV2Lv2q9eLIvSfAAMAaacnllcFBmYAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-ppt { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAkhJREFUeNpsU11rE0EUPTM7ySZpmzT9DNamWAtFfSiCigr+AxF9zKtv/hvf/Aki+FEi6ov4ItWHPGiwiBUKoUqqTUJImmR3M7Mz3t0kNe1m4LIwc+65595zlxljEJzdR5uf5nLmsvZx6gSvtd9W9bjhF7jg5dH9nRc/wq8YXaTSJptb0xklx7IZoKUEz1zJ2DUU69/37vFYrDxegJ9U0lC+AoIIVGg9CL+vIObP48KDQn7x0sWiVnJrnEDg7KGk+i/Ac4iUM/R7BsmrSSxtXMfa3X7el8+Kjf3KfUJ+iRJQw4w0Tc8BRyWGRAZY3rBR/VlC+XED2ayDhZyXl03+hNA3TxNQshlGLAnE44zCIL1goXZwiMNvB1i6zbC0KuAsxNITWwgNMYPeLVJiFEO9ArjHAivrAjNzBr4f4vwIgdGD4YUACsZCE8AtYGWT5jCsGQw5wEYJzP/pj5RwYTA1b07eQmfZ8P0sgdaM2FlYwWkMgMpl6NQAO33GKM0wsQWflkh1uqGVmVWblsiDkQyqxwfag35SqcktaEWTUTHYNx4iGU/C29+BvX4Lpu/C7zYgFjegSY63WySsHyXwpYHU00ieu0bAOuJbBTArBkiXKiaAmTzcvRJUV9E8rOgqBwqlY8ASs/AadbRLb8CzeTjVClqft6FdB17tL7yeCbFRBYoLr6vR/PiSEl5BZJaBD0/R2nkOZqfQ2fsKt+0SEQ+GLSIEUvJm+6jbah2+pS2aon+4g/afd4SYJVuA7vvXdC/IHQtSoTnK+yfAAIEaId1m+vudAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-psd { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAqxJREFUeNpsU01ME0EYfbtdKKWGtoItRWgJHApCBE2I0YuoiSaaeDJeOJh41YN3TfTixcRwMfEk8eDJGA+Eg0YTTRRMg02KKFooCBbTlkJLS7f7P+u3K9Xo8iWT3Zn55s173/uGM00TVlwZfzJztD92iKO5ouvQGQPHcQDN380vlDPr65fdLj4Oa41i9sFt+ytgN7o7woGOrqgvvpLBaF8vWj1NUAwGTVNRM3mf5vU/zaU+XySQuTqIFXz9hxmGLkoS7r+YxvVnrzGzlgXPDOzUZPT4m3Dt/KlIuH9oUjXYEHZZ/wOgGQZi4TZcGI5hLb+FO++TSOSKcLtcMA0dI0EPrp4+HtnfG5skiUecDGwQE2MjAwiGWlFVNDz+tIyCokJhPKYSX7Gdz2I01hOJdnY9rJ/7UwPGTEiqjtbmJtw4MYx78S/4Wa3h5UoOYwPdIOp2Xi/t18rlFgcDw6o+ydiWVRwOBnCpL0oOAMmNEhLZIgSeoxwGSWcERon/M9DoBknTIdNQNAMnO4PIVGpIFXcwndlA2OtGc4MAxml27p4AIulWSIa9QVadiYSoJxhqBJivKgh5ad3k9gaw6JdlDaqq7q5wINY4F22HaLHSDZQkBW72O9cBYFEviBIURQH7a7MN0uDisUW12ZZcaGlmdq4DwCqeTo1zNtZuW7hUqGIw7MNqSUS2ImNsKEpSdEwt5lGhfQdAkQBEoub3NNrDJfAIeBuRrcrY5xGQ2RFJAjl00I8PCckJUCB9q1URBnk38XEJEuk41tmGwZAf66s1VOh2keqwoUnYpFxHH4iKIixkN3HzVQKP3iQR/5GDKMuYmE3h+fx3MHqh1sMafztHLuiCg0FAk0uFdLqcpGY5QEXbTC/j7mIaVjc18DxufUtBJ/vcggs+3ijVz/0SYABsJHPUtu/OYwAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-py { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAlVJREFUeNpsUktvEmEUPTPzTUFmgJK2UqXQFG3pA6OBLrQxamJcaYwuu3Dp0l9iXLvVtRuDpgt3JIYaTVSaxtRHsJq2xEJBHgXmifebMhECXzKZme+ee+65516h2+2Cn2cb2VwyHl12//vP2/zOQaF4uD7GWN69e/LogfNm7kUsPBFaXYwHMeK0OlpQEJApHJTuykzK98dE98O0bLM/UNgr4v32Dj1fwSQRt9dSsfmZcMa0rIv9ODaqYrPVxuPnL1Cu1aEbJu7fvIZUIo4bqeVYRzcyv/8c3SPYpwECt/dmu4ON3Ed4TymI+hQc1ZqoE+F+uQLDsnHlwkKMscJTgl4eJOi9fxZLePNhGx6ZQRRFqH4VjZaGSv0Y6cQcJLpra0ZguIWegqDiw7lYBBZV6xiGk9DQDLzK5bEyF4Hi9VLMsoYI7J6Es5PjeHjnOl5ubqHaaJGBEkzbxplQAKIgDmBHekDTgI+qKKqKLvNApgmEgyquLs1CoFn2Y4cIeLJpkjoCLkWnUSIF3JxISIUsCjAoxhWNJLBIJs3YeXj/08oYZkOKY65HllE/bkMmY504YUd40HUq2JSSyW6iVPmLiXE/ZMYQCU+hXK3h1toqdNN0sEObyKtqtDQ6kXDwcadDS2TBryp4nX2HxXjsJK6bDnZIAZem6Tp5YMMmicn5OC4lztNWtvB9cg+hQABtWjKL2jH/T3GgBcYDXEE6mcDM6SlaJAGMWkivLBC54ZgniZaDHSI4rNSqn7/t1vgkGJPwZXffSeCjk2iUWz9+nSTQN8e6ef8EGAClUi/qoiOc3wAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-qt { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnVJREFUeNpsU8tu00AUPU5sp41NkzRxpfSZqi0VIIQqEEJUZYXECvbwCWxYsuBD+ABUFrDrCnWBQEJdIWigBSr6pqRJ1ebhxrE9M7aZmSrQ4o505fHMnXPPPWdGiaIIYrx89GKpNDdxmXkU3aEoCsT+z8W1Sm21+jCpJctQTvaerj+TX7WbnJ+0cpfuX8mQtn8GgJ4AZtIFY2Hz3foDVRcgyt+cRHcS0IARh+D/8G0PpmVi7smd0dLs+AIjwTVEiANEYYQwCHlEZyJgIQKfoX84g9uPZ0cHZ4YWmE9nuufU0wABCSSImMsWEgqSuoqA/39/swZFTWLy7vQo7dDnfPvWWQa8GuOV3IYLJXmyzDzG2/ChZ3pwbHdQ267BKJoYuj7SF2MQhiF8LuDK/Gf0DKTBKINz1IbTbEMzU1ANDW7LAfEIQKIgBsBFlAx6LYOz6MAcvoDCtAVGGPKlAiIu/F55F33FDA6W93EOAOMaMOl7biKPwRtD8Foetj5sYPfTDtxjl1f3Ubo5jkQieQ4ACSUD2iE4XDpAdbUiW9D7UsiN9WNkZgxajwbd0LGzt3keAJPUc1N5SVeENT0Ao2BKV6QzwlZeRBSKAYhe3aYHcZWn7l1EfjyPypcK9LQGa8qCvW9j9+MvaasQOHaRhGWdhsNLR8hwodYWf6B4tYjDjSOovRqq32rSYq/lytw4A77o1V2ERiAtzY5kkUrrsH+3QF2KY87ArTtQuQ6nAf4x6FCV1D001+vYersBM2vA4y1Rm2D7/Rac/TZIw4d/6MrcGAPf9htN0miJh7Lyuoyvr8rQeP9iVJcrSKgJ+TrFcyYebXTP/RFgAFQobmIOBxbsAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-rar { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnpJREFUeNpsUktPE1EU/u68OgylZXi0hZACQU1LEKKCMcat7jTRnQsXxsQtv4E/4M74P1iriUaNCw1FgxpjCJQKKAU60+m8mJnrmSll4XCTc8+959zz3e88GOcc8aq9evChOHl/lvMoubvWX/z4+BwTlbvw7bXdg8b7h6LE1gGW+O88CRMt4XTlR6/rYxce5Xv3jlHH19fPkBu+gWy5mlcFb3Wn/umeKOEMJF5C7xCFbtA9dRXjFoYKGiTRAlPGUV1aKU9O3VwNQ74A8DQAIZxqAuAhBPIMFYpQVAVB4CPSZjEzv1weH5tbDQN+JQ2Abu488mnzIbAAA3o/VK2PwDJo7r5Fy7ZRuvi4PFS6+qIXdVYD8Jg6BUcuOD8BozSLlRWyicgVKkTMQWwUlFF0Ooe5FIPk57BD7G0SiywyjD8bCDyHsOkeeeR3SUxEkROmU6BfQYFJMHfhWXV8efkUrb13VPMTsrcTQSzxZ/+n0GVA6EGbSGdgG9vo15fg2nFgbO8k70SRdd+mahDT81vUxTZRlJBRMsjq89C0EXCvSf7TIBZ136YZUJEiE7LgJ2dN01BZuE0dkIhxE7KcQTK1QUj+cwAEyrPZ+IydzRoyah+mLy2isbWBweESJEnB9q+1RM9Ub9GQOWkABg8HjRr2d9Yh0hTlBlRsfn+D4vg0BvUC9rZqECUJuk7Tzr1zahCYlB6HJAREPwfbbMBzLBzsbUKVI0qBgQkc+SxgWUYaIAqOpKwKXJ6bgGlaaDV/YvHaFNrtDsKTfVSrJeqIg/bRNwjclFIALeP3saybhu8SC4VBHwnhBXXIKocYRXD9QzBi4Xgchmkd9+L+CTAAMqwy+ZzluBgAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-rb { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAixJREFUeNqEUktvElEU/mag5f2yJhXLwxIt0kiqsVEXujP+A925cu1Pce3WtXVtYuJCF7KtTY0NrVQIpRVKeXTkMcO9F8+9ZVooJJ5kcmbmfOe733fO1YbDIWS8+/g1dycVX7W/xyO3vdsuVKqvnE7HZ230783rlyo7bVBicSGyfjsVwozomVbIPe/c+FmsPHfoRKJd1HT7hXHBZjVbA4aA14NnD9bC2VR8gwuxPi5Sx39Cp+M0XUP0ahhP1jLhW7HFD4zze3b93ILtXYyyVKlR8/5hFbnvO9gtlrGSjOF+OpXkYviWyo8mCS4R6bqO4p86vm3v4fC4DrPfw4unj1XN6JvBaQtjChzUXK43sVU4wNFJA43Tv/B73edQwTmfIhAjCVL6UdPAj1IVFSKhCdAcAI9rnjBiAjtBYEu3GEeh1sKJ0YXR68sVIujzIhzwY8DEBHZqiLRKkicQDfvABxaiQTc4Y/C65pCOXwcjcmlvJgHtlwi4epYifiQWgmoLZwPW6HQG07LgcOgKO0UglAKOTt/E+09fwAiUWU7QAE9xUK3jbvomsispZVHMVEDSZdHo9rCZ/4VIMKAu0XGjpU7d2S8hk0pCELHEzrjKnCQOYJoD+Dxu1RyiwUm5LaMDo9NFt2cqDLvY4oQFp/QpfT/MrmI5FkWebt+NpWto0j2QmQkOjZ9hpwhqjXZzM/+7LU+cc7lRrjXh8/lVLRK5ovLWXglOsiOxdt8/AQYAzv8qbmu6vgEAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-rtf { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAe5JREFUeNqEU01PE0EYfnZmd5FSvgLYFuwWt9EgHyEaox68eDJevHvwJ/hTPHv1N/QgZ2NC4g3kUAQKFKGhjVKqRrvbnRlnht262FHfy+y8877PPM8z71pCCKh4/ebt+rJfXEz26Vjf2mnsN5rPKKWbVpx7+eK5Xu2kyMtNTd5d8MdhiJ9BOO7atFI9ajy1UyAqSPIRMR6ZmoNehNHMMB7fX/UWvEKFMbYKE8DfQnAhwRmmJkbx6M6S5+WmK2Evup2c9yUk2nnKA0XVcSiGXAe1k5beP1i+4RFCXqnPywB/AKVzK34RjHNYlgVKCH50w7EBBogbTa/AVM5SgBdn0gc2AMDjPsbFPz2xye9asweS6n+NTbG8BCCfUtLjff2WoVnVpAH6z6hMUtJE3EykYfpF4vUiL3QNS7FMeSAQRBHW3r1Hq91B+VoBQRji4+ExFsvz6Hz7jm7Yw5OH92AcJKW9G4SoHhzhy/lXbB98Qmm2oCXN5WawsV2TACEoJXqwTKOsb3BtR2ucmZxANpPB8JUhyPnHWDaDpfJ1eZFALzJJ4MKO5MEtv4TSXB7V/br8iQLMz+almRZWbvoo5q9qRlxwewCgeXbe3qrVO5ZkUD/9jJGRLPaOm6COi92TU1DbxYe9umRD0DrrtJO+XwIMABWp9nS+FgaoAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-sass { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDNDMTBBM0JGMTE5MTFFMTg3N0NFOTIyMTQ2QzhBNkQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MDNDMTBBM0NGMTE5MTFFMTg3N0NFOTIyMTQ2QzhBNkQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowM0MxMEEzOUYxMTkxMUUxODc3Q0U5MjIxNDZDOEE2RCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowM0MxMEEzQUYxMTkxMUUxODc3Q0U5MjIxNDZDOEE2RCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Po72XUcAAAJcSURBVHjahFJdTxNBFD1bykc/ttvdtttWGgI0bYrUgDZoNYqRJ014kMRXHvwB/hQTH/wFhMREJfFBQxBjhMRIFEQSCAlQxKYGggiU3e3HbnfX2bFt1EU9k9m9mblz5p4zlzFNExYmpue/jmTSZw5PZAl1MAwDT0c7O72wvPdudeNakPNtOZ0tsM7cvzdOc5yN5LDAsTFRAJks/kC2PxFRVe39Si6f4byez62EpAEH/gNN18F53Ri/Ocxf7OtdLMpKT42s/ZPg1cISJp/P0tg0TBzLCoK8D7eHh4RkLLJ4cCz12AjMXwgez8yhqtVo3NbqRKlcxcSL16gZwJ2Ry8KVc8kZO0HdTKlURn+8G6PD2SZhLMQj96WAiMAh2RXFYKI78lcJcx9WYBCycICnpNbojUWpD5Y0C4Zh2D0w6hWc70uQZC+IWfQZrXF0IsHvY+meBd08haAhoVMMQFJKWF7PNZM+klhRyogGhbqxOIXAMOtEwGAqDqVcgbVkkE+5UsEAWavf0az2t0ZqvK2qabh6IU3joizDwTgwej1LdVfJXkdbK8mt2QkayO99A0/0trQ46I1lVcX+UREhnsP34yLp1AD1xibBMuntpzU8mJyi3Tc1O4+l9U06n7x8Q/8PHz1DrrALt8tlr0CrkbJMHTop9Sk5sLa1g8L+ARJdnShKClY3tunN69t5iGLYTlCtakjFY7gxNABdN3B37BaqqoYT8pyX0in4ORbRkIA46YlDRbUTbBZ2Jb/Pw4qiKFnapcpPo9pdbrg8DjAOBsFgELJmsGs7eWkkc5bu/xBgAHkWC6UPADTOAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-scss { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RkM4QjYyNDVGMTE4MTFFMTlBREZCNDNEM0ExMTk0MUIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RkM4QjYyNDZGMTE4MTFFMTlBREZCNDNEM0ExMTk0MUIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpGQzhCNjI0M0YxMTgxMUUxOUFERkI0M0QzQTExOTQxQiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpGQzhCNjI0NEYxMTgxMUUxOUFERkI0M0QzQTExOTQxQiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pkf1yeMAAAJbSURBVHjahFNdTxNBFD0tLULpB91uodVWPmorUIxo0VSiNSExMYYHE33l0Ud/in+C+OSjYgjRGDBRCKJIUkIEWi0WKlja0ul22+5219lJ26gLeiezuXvn7rnnnrlrUFUVms3Mvd2bjIyezRVLBA0zGAzo6jhjm1te+7EU37rFO+w7JlMbtG+ePJ5mOaZmci/nsPl6ONBtw18WDQc9tZq0sp7YjTisXV/NFKRpRvzHpHodDqsF03djzuvDg6vHJWFAprF/Arxe/oins6+YryoqCiUBvNOO+7FrXMjnWc0WyIAOQP0N4Nn8IqqSzPx2swllsYqZl28gK8DDyRvcxKXQvB6gISYpiwgH+jEVi7YAfW4nEqk0PJwDofNejAX7Pae2sPhhHQoF63U5Gai2Bn1epoPWmmaKoug1UBoMrgwHabIVVCx2jdrKFwm67TZ2plldPQGg2cK5HheIUMbaZqKV9In6giDCy3MNYXECgKI2gICxoQAEsQItpNCHWKngMo01arTY/jFIzbutShJuXh1Fm9FImYiM7tTtKOtbO+toN9Nc+fQ5SGUOIVYl7HzPIH2YRZ0y2KZ+sVzBHn2v1mpMGx0DTaR3nzfwfGEJdybGkdo/wEigDyvxLzg4yiESvojZhfd49OAeLJ2degaSLIPOO6vwgiYaaRErTRREEdn8MeJbSVZ5M7nLdNExqFLaQwEfFfACQn1+HBWKSKb3MT4Sgstuh9vVDa+bQ4DORE6o6RlspzMk9TOPfr+fiLJCLFYr3TZSKNcI7+aJwWQmPM+TkqRg49tu65f/JcAAMwMas6WUKd8AAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-sql { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAh5JREFUeNp8kctrE1EUxr+ZyXMkoa1NBROaSkpTBE23PhZ25cql2y5duvAPUdGFS1FxIRRBXZlFQ9GVdDENIhGJxkDsw2mneZnM83ruNZlOmNoDhzlzz3d/9zv3Sowx8Ch/qlYK2XM3cEJsbH0+qjV/rd6/u6aN18b7RMFT+9aosP/Ex+0ae/puw7j36PlKEMAzctKJ3aGFamMHjV0d+wcGitkMrpWWp6hVIciEk2MAOwbUWjosx0UiFoWqJpGMx5DNzODq5aIPoa82AWBg/lyKLMH1PMp/a9XvLXLzG1cuFlBaWpiKxaIPSLY6CaC93ggQjyiQZRkeQSzLRovGaPciWLt5faSWEBoh6KBvOhiaNga0+Y9pwaFxvu7rfp8F5pWDt+qNMp2IijHGwddWCvN+33/CoAOP5nVdT9SdoQ1JkggiQ6Yvr7V60+9z7akA2gfH9cRF8hO5F5Ve4lQAF9uuK+qFsylkzsQxrcaQm04hdWkR83Mzfp9rQ3fAFzu9Ph6+WMfjl6/pGBdb2jbKmx8QlRjWy5vkyhUZBPgOeGNHN9AbDLGUz6He2hVj3Ll9C8/evsdgaMK0HV8bcmDTU0UUBYXcedR+NLGnH0I3jvDk1Rsy46FP4C/1BtrdntCGHNiOAzWZgEKQ5Qt5lIqLojbaXSQTcRy2OwT4SZqk0IYAOgkVWUE+lxX/zb0DpFNpkTzmZmfFtzewhHYcfwUYAMZmVaZQlLFHAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-tga { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnxJREFUeNp0U89PE0EU/ra725K22ILRGipb22pMG6JcSEQTbUIwnozxpBcvepeEP0KPogcT/wlNT17kIKbEmChFUYKGVtL0R2gLtNCl3Z1Z3+zSAlonmezOe/O+973vvZEsy4JYnqdPMu6RkSQYQ29JEkB+PZcrslrtPhQl23VZc8/tr9I1yMHg0EA8HrBM04lVFAhoY38fSSDQVN3pfKV8G7KcxZHl6v1xblqU3eLc3p2VFZjr6+gQgwsnhzGTuq6Nhs6kYZqXjwL0GFhEl3U60OfnwWs1GGtrUKNRsKkpeIIBpKIRtI1J7cX7hXRhc/MOhXw5DkCZGG2zXAajzFIoBMvng1ypIKOqmP30GW3OIEcimovzlxRy5RgAFwDEAIODkCcmIMdiQLsNdWwMZdJlg8pzEUt1aBhKq3XinxKYqF9yQbqRIqsMy+0Gyy47bKgUWXSLtDENE5wdtuqQATm50F1VnPbRGeEw8HXZbiV8fsDvI9ldju9vADAyihLEbrWAZhOoVp3z6iqBUiB1A4nEfwCEsbkL/M4TgE5n5jDx+oTEzp1d8m9tC8H6MaAB0imzx0NU/WKUYE+loEyawDBo2ui6TGfT6ANAxrvx87gYCGCxXEKVJvCWFsG3eh1vN/J4OD6Od4UC8o0G3TX7TGLHwI9iEQmvF9X6Fh7F4/iYy+GcLOMSlfEgGsP0qdNOmX0BiGKpVkV1bw/1nW2b/gCpf1PTcI+Y7eg6ps+G4bG4PR99SjAVo9HE4q+fKNE0vl5awuSohjeijbRefVjAtUgEQRK7Yhi9OKn7nKWZxxlSPWl3QwgnaIrW8QMhD542vUbx/W49m7sq4v4IMABOqi3Ej7bAEAAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-tgz { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnhJREFUeNpsU1trE0EYPbMzSTfdtInFtkkpiaXVWou2FRUEn/so6JugL/oH/Af+B1988if40jcFERQURNBSQdDWlLQN2lsue8neZsZvc7FoOrDszM75znfOmVmmtUYyvry++36yfOeS1qqzDtvH2P76ApPlW3Drb2sHex/uccHWAdbZX30kO2+B3siN3zhTnHuQ66+95i423jzFzOVljBdKOZNHazvVT7e5wF+SZBj9iZJ+3J11mbW2kR8T4LwFli5i4fqTUvnczTUp9RLtDhKgJx0q4dEwWAxrREKICHEsoYYXMXvlcWmquLgmY71yCkG/c0AkARgLMZpnMDMpGNzEYe0dGp6HwvmHpbHC1Wf9MnFCkHQOyYEPzSJwQ2B65Tm5NZG3Fshim6wbMNJn4bpHowMKtIqo2COgR2IcAptwjvcgo6i77igjEmVDqbY8xQJ1VwRULhiBI6+G9Zf3cbTziuzIDkmHSNqECTFgQScEcYuc2NA8TcdYwXD+GkK/TYVN+u72WrIudiAD8o6oAR2RRCmQMjis3CIy1iSpPySCXhFTXeyAgh4BR+JVw8pauLi0Cp4yCX9A90FQhnSBYtnF/k+Q+HYam9itfIZB3QvT8zj8XSW5EhNTs9ivbSLwPUzPLNPJBIMEKnaQYg6aB9+RGR5F5VsNgnNKXMI1NdJGG5WfHzFVLJ7k8c8xUngpVodlDSGbFYj8Y4yMpOG09lHf3yIFPzA3fwHZTAQVtU4JUTeFDrdgDdlI8wAz5Qy2KxswReI7QODZcOr0ZH3q2hIDBI7zq16tuk3FNPxAI4wN+pkoccYoE4YJU5EdUtM4Qst26v26PwIMAKj3P/2YUKgYAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-tiff { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmRJREFUeNp0UktPE1EU/qYzHWstlrYJNcWUElyUJsaNGh9B0g1Lo0v9Ey78EbrVxBhXuHShm25YGBJRQpAYBDEWpaEPEhksdVpbyjzveO4MfZDCTWbauefc736PIziOA77OPH2yJCcSGdg2uksQAKofFou/7VrtASRpvVNynj13f6XOhjg8HAlMTIQdy/LO+v3uYUPTkAHCTb+cK+0pdyGK6+hbvu4/xiyHbncYAwfR19ZgbG/DoO9LsSgeTd9JXoxfyMG2rvQDdBlwIZauQ5ufh12twioU4E+nYU1NIRCNIDs+Bt28mXzx8VNuZ796j9q/DgAwomwqClilAmF0FE4wCInAlkjO4y+r0JgNX2os6XPYS2q/cQyAcQatFjA0BPH6NYipccAwIGUy2CVJFZInkKlyJAqx3T4/IMGmJkeWIWSz5KgI5pdhb3yDXS5DSCYh8rTID8s0wexeVD0GtMd85KkkefFxUfE47M1NokbJkByEQl6tL+ouAI+MUwbFhnYbaJKc/Sqg0x4H4eDRGDA56fUOABA9/GsCpaIHwr8FOhQ823O5RfW66tUGADhNy3RNRDjcN41HLxdQ8J6jYTsOQLfOJBK4f+s2/uoathoNGKT1MtFeVHZxdWTEZfEq/wMKl3rCJOIzTV6ADs2R5ulYDDNkYjp0DhrF+zCVgkw31+v1UxjQZkNV0SADd2o1MIuc9gmY+/kLxb0/UFoHePd9A1qzeUoKpilx9xcLWzgg+u/zeVfuQqkM9bCN1ysrWKXxdtPgvScwUAm58XZ52W16QyPtifRUzi588GbEi1ztHPsvwAC4uC9qhnsZvwAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-txt { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAeJJREFUeNp8UrtOG1EQPfsyXiyzBguIJSyChZBBEFCKpKHLo6egpErNn8CHgH8gkZIiTSIXLhJAWCgkoMgRMSiRBSK29z4y9+I1d/HCrFb3MTPnnjkzlpQSynY+fP70fGF2gQuByCz6lfdd9Uurfvrrjes6762eb3tzQ69uFJwPsqOPC+MBEmxxphi4tlU5OGmsOzaBWLc+O9oIIVhScidkyGZ8vH62nHtSKlaI4cse6TjAfSaFBBcco0EWqyvzubmpyQrj/FXk75cQaSEMeMXU8xykPA/Hjd/6/LRcyjEpt2i7HAe4A2TeLZWKUOJaVLxj27j813EHGKCXaAJExu/4BOdiAED08riQD2riOrexyRoYc3CvsAbLGAAjZga7vgZG23WMCdBvoxKJc36TRBlMiaa2JByjNqqD8qkYc1pjDK7abey+/YhrWlfKswhpiCR96aEU9o5+QE3g2ovVWDm2Sc22bBQm8vrVpbkS9r+doPr1EOWZaQ0yFoxg2PcREosEAI4uvZhJpzFMP+cSXRbq+043RManez+tNWKMI6GN0g0Z04HFR+NoNC/0yx717efZOSbzY3AcR4Op2AGA5p/W31r9e0vNgSrh9OwCrpeCkqvZuqTybnpRqx/r2CjvvwADAJC/7lzAzQmwAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-wav { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAApFJREFUeNpsU1tPE0EYPXtpKbX0wqUQKVQMFdIXQBNCQBs06KP+B8ODGh+Mf4b/4IsGE54kxhcMBrkp7YOQgBRvSKG73fvsrt8Otoask0xmd+b7zpxzvm8E3/cRjPkniyulW0NFy2JoDkEAguOlpXJ9p3L8MBqVl4O9YHxae8pXuRlcGO7KPLhfTDVUqwUgigJMy4Whm6lEXHjxYf3XnByRN0QB/2KaH7btMlUxoRJAcyqKhdOaht7+DJ49n+2cvTnwynXcsb+kLwJ4rgfmMDDGWqvneXCZS9ND7mov5h9ND85M9y86Dpto5rUkuJ4Py3YDJpy6QGJPayqB+Njf+43XL220t0cwOZkfrNXsBUqZugDA6CbLdAiAwaek1ZU9LmP8Rh6S78GsGxjOp9FdzKJaVZIhBgGASzK21w/wbrnCk8euX+EMAjaaZuPHdwUdHVFYluuGPGCORwwYjg5rqOwccRk+3Ux0IEvntmsNG4ZmUayL/wAwKHUNfZfTKN0ZRaw9Cof8qJ/pMAyHy5KkAMTksSEJtnMenM7EMVMawbejMzJRh67bXEYiIXEAVTW50SEAhzqwfqrBcXx4VOhYm4RsNgHbsJFOyZTsQ1MN+hcohoUlkFiMT+TQFpMwXOjGpXgE+XwGk1N5pFJtKNCequgYGupCRBbCDOp0KBJc4VoP3dyBONW8uydBgBHUThqQKCk3mEZ/LoUG+RBioJO7VarAwEAntjYPiUUW9Hh4b2R7k9j98hN37xWx8fGAt3eIAdVMLn+uUv+b2KReSCZjZJiB9bV9jIz2ofr1BKvvd7G9dRC80lae0HzOt+cWVnrSKDrMJykifwNBpCgE/UAllEXufmDu8Zlffvvm8XSQ90eAAQA0pF7c08o4PAAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-wmv { - background-image:url("data:image/svg+xml;charset=utf8,%3Csvg id='Layer_2' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 100'%3E%3Cstyle/%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='36.2' y1='101' x2='36.2' y2='3.005' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23e2cde4'/%3E%3Cstop offset='.17' stop-color='%23e0cae2'/%3E%3Cstop offset='.313' stop-color='%23dbc0dd'/%3E%3Cstop offset='.447' stop-color='%23d2b1d4'/%3E%3Cstop offset='.575' stop-color='%23c79dc7'/%3E%3Cstop offset='.698' stop-color='%23ba84b9'/%3E%3Cstop offset='.819' stop-color='%23ab68a9'/%3E%3Cstop offset='.934' stop-color='%239c4598'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill='url(%23SVGID_1_)'/%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill-opacity='0' stroke='%23882383' stroke-width='2'/%3E%3Cpath d='M9.1 91.1L4.7 72.5h3.9l2.8 12.8 3.4-12.8h4.5l3.3 13 2.9-13h3.8l-4.6 18.6h-4L17 77.2l-3.7 13.9H9.1zm22.1 0V72.5h5.7l3.4 12.7 3.4-12.7h5.7v18.6h-3.5V76.4l-3.7 14.7h-3.7l-3.7-14.7v14.7h-3.6zm26.7 0l-6.7-18.6h4.1l4.8 13.8 4.6-13.8h4L62 91.1h-4.1z' fill='%23fff'/%3E%3ClinearGradient id='SVGID_2_' gradientUnits='userSpaceOnUse' x1='18.2' y1='50.023' x2='18.2' y2='50.023' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3ClinearGradient id='SVGID_3_' gradientUnits='userSpaceOnUse' x1='11.511' y1='51.716' x2='65.211' y2='51.716' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3Cpath d='M64.3 55.5c-1.7-.2-3.4-.3-5.1-.3-7.3-.1-13.3 1.6-18.8 3.7S29.6 63.6 23.3 64c-3.4.2-7.3-.6-8.5-2.4-.8-1.3-.8-3.5-1-5.7-.6-5.7-1.6-11.7-2.4-17.3.8-.9 2.1-1.3 3.4-1.7.4 1.1.2 2.7.6 3.8 7.1.7 13.6-.4 20-1.5 6.3-1.1 12.4-2.2 19.4-2.6 3.4-.2 6.9-.2 10.3 0m-9.9 15.3c.5-.2 1.1-.3 1.9-.2.2-3.7.3-7.3.3-11.2-6.2.2-11.9.9-17 2.2.2 4 .4 7.8.3 12 4-1.1 7.7-2.5 12.6-2.7m2-12.1h1.1c.4-.4.2-1.2.2-1.9-1.5-.6-1.8 1-1.3 1.9zm3.9-.2h1.5V38h-1.3c0 .7-.4.9-.2 1.7zm4 0c.5-.1.8 0 1.1.2.4-.3.2-1.2.2-1.9h-1.3v1.7zm-11.5.3h.9c.4-.3.2-1.2.2-1.9-1.4-.4-1.6 1.2-1.1 1.9zm-4 .4c.7.2.8-.3 1.5-.2v-1.7c-1.5-.4-1.7.6-1.5 1.9zm-3.6-1.1c0 .6-.1 1.4.2 1.7.5.1.5-.4 1.1-.2-.2-.6.5-2-.4-1.9-.1.4-.8.1-.9.4zm-31.5.8c.4-.1 1.1.6 1.3 0-.5 0-.1-.8-.2-1.1-.7.2-1.3.3-1.1 1.1zm28.3-.4c-.3.3.2 1.1 0 1.9.6.2.6-.3 1.1-.2-.2-.6.5-2-.4-1.9-.1.3-.4.2-.7.2zm-3.5 2.8c.5-.1.9-.2 1.3-.4.2-.8-.4-.9-.2-1.7h-.9c-.3.3-.1 1.3-.2 2.1zm26.9-1.8c-2.1-.1-3.3-.2-5.5-.2-.5 3.4 0 7.8-.5 11.2 2.4 0 3.6.1 5.8.3M33.4 41.6c.5.2.1 1.2.2 1.7.5-.1 1.1-.2 1.5-.4.6-1.9-.9-2.4-1.7-1.3zm-4.7.6v1.9c.9.2 1.2-.2 1.9-.2-.1-.7.2-1.7-.2-2.1-.5.2-1.3.1-1.7.4zm-5.3.6c.3.5 0 1.6.4 2.1.7.1.8-.4 1.5-.2-.1-.7-.3-1.2-.2-2.1-.8-.2-.9.3-1.7.2zm-7.5 2H17c.2-.9-.4-1.2-.2-2.1-.4.1-1.2-.3-1.3.2.6.2-.1 1.7.4 1.9zm3.4 1c.1 4.1.9 9.3 1.4 13.7 8 .1 13.1-2.7 19.2-4.5-.5-3.9.1-8.7-.7-12.2-6.2 1.6-12.1 3.2-19.9 3zm.5-.8h1.1c.4-.5-.2-1.2 0-2.1h-1.5c.1.7.1 1.6.4 2.1zm-5.4 7.8c.2 0 .3.2.4.4-.4-.7-.7.5-.2.6.1-.2 0-.4.2-.4.3.5-.8.7-.2.8.7-.5 1.3-1.2 2.4-1.5-.1 1.5.4 2.4.4 3.8-.7.5-1.7.7-1.9 1.7 1.2.7 2.5 1.2 4.2 1.3-.7-4.9-1.1-8.8-1.6-13.7-2.2.3-4-.8-5.1-.9.9.8.6 2.5.8 3.6 0-.2 0-.4.2-.4-.1.7.1 1.7-.2 2.1.7.3.5-.2.4.9m44.6 3.2h1.1c.3-.3.2-1.1.2-1.7h-1.3v1.7zm-4-1.4v1.3c.4.4.7-.2 1.5 0v-1.5c-.6 0-1.2 0-1.5.2zm7.6 1.4h1.3v-1.5h-1.3c.1.5 0 1 0 1.5zm-11-1v1.3h1.1c.3-.3.4-1.7-.2-1.7-.1.4-.8.1-.9.4zm-3.6.4c.1.6-.3 1.7.4 1.7 0-.3.5-.2.9-.2-.2-.5.4-1.8-.4-1.7-.1.3-.6.2-.9.2zm-3.4 1v1.5c.7.2.6-.4 1.3-.2-.2-.5.4-1.8-.4-1.7-.1.3-.8.2-.9.4zM15 57c.7-.5 1.3-1.7.2-2.3-.7.4-.8 1.6-.2 2.3zm26.1-1.3c-.1.7.4.8.2 1.5.9 0 1.2-.6 1.1-1.7-.4-.5-.8.1-1.3.2zm-3 2.7c1 0 1.2-.8 1.1-1.9h-.9c-.3.4-.1 1.3-.2 1.9zm-3.6-.4v1.7c.6-.1 1.3-.2 1.5-.8-.6 0 .3-1.6-.6-1.3 0 .4-.7.1-.9.4zM16 60.8c-.4-.7-.2-2-1.3-1.9.2.7.2 2.7 1.3 1.9zm13.8-.9c.5 0 .1.9.2 1.3.8.1 1.2-.2 1.7-.4v-1.7c-.9-.1-1.6.1-1.9.8zm-4.7.6c0 .8-.1 1.7.4 1.9 0-.5.8-.1 1.1-.2.3-.3-.2-1.1 0-1.9-.7-.2-1 .1-1.5.2zM19 62.3v-1.7c-.5 0-.6-.4-1.3-.2-.1 1.1 0 2.1 1.3 1.9zm2.5.2h1.3c.2-.9-.3-1.1-.2-1.9h-1.3c-.1.9.2 1.2.2 1.9z' fill='url(%23SVGID_3_)'/%3E%3ClinearGradient id='SVGID_4_' gradientUnits='userSpaceOnUse' x1='45.269' y1='74.206' x2='58.769' y2='87.706' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23f9eff6'/%3E%3Cstop offset='.378' stop-color='%23f8edf5'/%3E%3Cstop offset='.515' stop-color='%23f3e6f1'/%3E%3Cstop offset='.612' stop-color='%23ecdbeb'/%3E%3Cstop offset='.69' stop-color='%23e3cce2'/%3E%3Cstop offset='.757' stop-color='%23d7b8d7'/%3E%3Cstop offset='.817' stop-color='%23caa1c9'/%3E%3Cstop offset='.871' stop-color='%23bc88bb'/%3E%3Cstop offset='.921' stop-color='%23ae6cab'/%3E%3Cstop offset='.965' stop-color='%239f4d9b'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill='url(%23SVGID_4_)'/%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill-opacity='0' stroke='%23882383' stroke-width='2' stroke-linejoin='bevel'/%3E%3C/svg%3E"); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-xls { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmxJREFUeNpsU0trFEEQ/mamZ3Y2+0zIC2MmITEkUYgERFQErx5E8KTi1b/h79A/4SW3nCNeYggBYZVEMU/y3N3Z7M7OTD/G6lk2ruw20zRdU/XV91VVG0mSQK/3n1a/jky6d6Xs3G8WXS+Pw5N6LXjLLGuna/78oZKerGsYKtrDE16uJGL1L9gEOOcYd2dL1fNwrbL//aXN7J1efPMmkUqEFAk0A0VZNbFEaQCBscIkXj975y3NLq9xye8PBkAniHOFph+j2eC4rsdoB4LsFubGl/Hq8RtvYWpxTQi52o1jvWiGYaRZL0/auDgOkC/Z8BYL2Pqxidp1FZkhoDxpeaXA/Ujuj/4HoOxKKjiOiek7RUShRNQWaNYFQuMafrYCxiw4ozZKfqbYJ0EvRdl1DQyyTs8XCNTA6UELMwvDyLpZWIZNNlNLlQOK2LMJRJ+5AkuZ1S7CFFzJzk56GnUjQWlYkqCoBWFbonEVYcLLA4dNnB624GQsDBWIgfZJEgxkoChzSFWvn4VpQemDm2VwXQsXJwF1h6c+gxlQ5jgSiEUEt0wdIe7tMES+nEG2aCLiJMOIIWIr9e0DEELAMUrwRuchVAyTKimUwO75Jm6VF3Bv7imOaj+xd7UFKVS/BPJF1b/E4tgTrE49J60O5kceoNqowiuuYKa8ghHXA48U9MT2AQgyRvTThE30bQiaSGa4yLMJNFo+Dq/2cHt4CYlwyFf2S6BHwwrMw/avDbR5C1k7h1YQ4KH3Amf+AcZyEbZPv9CItzQD1l9EbtYOjv74v/d3O9RMPTDrsEwGIWN8q2yk7XNYRs9JrRv3V4ABADSGR6eQ0/NQAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-xlsx { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmlJREFUeNpsU8tqFEEUPVXdPY/ueWZIoiYZiSYKYhJc6EbduHOhgijo3t/wH1z6B0JAhOyMILhxo4kJGk1ASTAxwWF0Mpp5dHc9vFUzYwidaoqmq+8959xzbzGtNcx69PTS26ETmQtS9r4Hy/xv7MW7jV+th5yzVcaYPX/++It9u4NAv+CVR6tBUUTqMJsDcRzjZOZM8W9ZLKx+/XDb4e5/kH5In0lpIYWGUaC0YTZnBCAEKoVR3L36oDo7NbsglZwbqD6iQKOXFMcKUVfBkBAoQhlD5xxMDp/HrSv3q1JgYW3z0x0KXzkCYJaRZljru23aHWTzLiamAyytv0O9UYdf5PArqlppBfMUfu4oALErqZBKcUxMFRCHEp0DgW5Lo4N9NIN1dF0XXsVFOUyPJTzo+WBANDidjp8tgHGG3c0DnJ4uIRf4cOCBaW5KjY8xkZL72xpJ9QcFz5bVqHUJGHZL2YtNmKi06YCyiVFb4s/vEKMTAf1p4edOG6mMi1zR6wEpdUwX+vLDtkCzHoK7ptcM6ayLmGajvtex4PliyoIkFRjmUEASelB2rXQRSfjUCT9PlWpmW21iTGzCAyEkUixPRqXhe2V4zKczbdmybgkpJ0cGOuA6Y2MTCsKoi5HsNK7N3MN+uwYaWbxYfoLLkzdxcew6lrYWaZhm8PHHG3zffp1UwJSHz9vvkU8PodbcQYYYS5lxYkxTkGdVDQdV1Js1qPgYD6JIuIE7gsXVefIhIuM05k7dwMbeMmh87a18ufIMaVYyprrJLgje2Nr+1tzYXANnDnr3zRhHj37Vvy2wpXHtNAd5/wQYAD6WMuT2CwoVAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-xml { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAilJREFUeNqMks1PE0EYxh+g3W2t1G0sEqyISynUFJsSOShNwCamiYZED3LgIkcuxoN/iCZePZiYGD2aGD+i0F5KMChxlVaakAK2ykcAt+WzdLu7zkxo3WZL4pu8mXfmeeY3885ug67roPFh5nvc62m9hjoR+5LMp7MrkYf370qVtco+VtCUFpbj+jGR+JbWn76OyQ8ePwsZATQb8R/hanZgINgj9IqeuBFCw1Kt9OMBnNWCs24XwkG/QKYUEiGjVAPQof/rq0783pShET3ULQo8xz0iS5FaANmrHQH2DoqY+DSLSz6RzecWlnD9ymU47LYjd4O5BXqDTG4FM3NpTEkpdJ5rw0AowLRMbhUfp58gTOaD/UHmNQPI6YmvKWRX1zESHUJ/oBs2nmPa+Mgw0ZIM3tZyGoJwygzQNB2jNyJIZX7iB0lpPoM70UGmPX8zCU+rG8NDVxHwdiC5mKsPUFUN/gvtLLf39sFzVqaN3YrC6TjBauqhXhNA1TQoqloV7Da+pjZq1FsXUCamF29j6LvYhf3iISamZ3Fv9DZevouhRzzPfOG+3hpA9U9UyioOlTJ7pFeTCQS6RGzIebyf+oz5pSzWtmSW1EO9phvQ00slBRt/8qR3DoWdXbiczUiTzd52D+tdLmyTB14mx1rMAKVcRpEATjrsuElee/HXGmnFRyBOGD30C/nEDjNgs7CDpsYmnHG3YPegBCvHs9oYfm8nG9dJa5X4K8AAQzQX4KSN3wcAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-yml { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAdxJREFUeNqMUl1rE0EUPbM7m5Y0Zptu21AwWwhYpfSDFh+kvvRd8N0Hf4I/xWdf/Q158F0QoQ+CVsFKaLSQpt/dpmvztTOzzky6cetOpWcZZvbO3MO5514SxzEU3r57/3GpWllM/tP4sL3TarROXuSo/SWJvX71Uu80Cfhlr/T4UdWFAVfdnmsTUtvdP35OUyQKVnJgXDBTcj9icAsTeLax7j/052qM81UjwW1QJXEhMF0qYnN90fdnvdogYmvJPU0/VBApD4hcDrWRcyikfB17srzgW7b9Rh1vEvxDlI4tVytaBSEEtmWh0xsUMwpwnWjqAlcxogiHd1wiQyCu87iI/+sJtf6+NXsgpd7FWCMB50KvkYMGMbLdZgLlfj+K9K4+FnFQ2x7WntIs50AbmiGwLILt+k+EvzvSNIHzdigdJ/AmXQRhiHv5POSwYmG+cqPVo0HqDxj8uTK2vn1Hfa+JmdIkvtZ/4fOPXU3WPDpFeNWVyUKryCiIGMN4zsH98gym3CIcOTwT+XHdXrdQQHAZotE8kBPpSqPNHtBOr48HUmLOcXRJT9dWNMGYJFby91pHOAvaykSaITg+bwefdhrteDRTMSwyrFCgI88E056Hy+4Ah2cXQZL3R4ABALUe7fqXWFN6AAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-zip { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAm9JREFUeNpsk0tv00AUhc+MY6dOmgeFJg1FoVVpUWlFC0s2IFF1jxBbhKj4BSxYdscPYcEmQmIDq0gsERIViy4TpD7VFzF1Ho5je2a4thOqNhlp5Mz4zudzzp0wpRTC8fPrk0/TC6+fDtYicLH97T1Kc2vQDcs+rH3eUAxVznn0fn1DRM8E+iOdv5ct3XmZG6yVlNj6solUbgVTt0q5FGtX6vXqC6VklTE+KAO/OODHSIQPRQpsXC+kkEz2ELA0ystv84tLzyucsbWByisAGf+QAS2CCDRRLMJMmxC+i8C4jdLCm/zM7OOKFGptcO6/BTpJ0yeQB0Y+mfKQuZZG0jQgeRbW8Xdomobs9LN8scc+UPHNy4Dwq8IljotIIQEm59/RoSyM1CKkXKZNBm7kIVgyM6wgAnSgRK9vqQfHPiMFDHqyFVsLR9Cm0o4YzoAASrSjCelQfRPb1Vc4qn0EY5L2W9GEaBLcxQgFHpGbkMIDJ69e+wjJ8VXqRgKid0r7ftQdxkRs9SqA2kgAm14SSIQh9uhuLGPMnKJs/5KquL1x0N0RCsizigoDaLqBdHoMiyvrlBsHVx1wphD4BCewoqxGKKDwAgtOy8JufYuk+5golGGaGZwc1sIGoDz3AOPZSVLaHgVwydoJDM1H4DbQODughB3YpOD44HfoHgnu4e7So0uAi0stHLJ3Aud8B9bpHu6vPoSu9TtDl6tUuoFiIYOgu0+158MKmOxomtyD3Qi/3MTR7i8K0EDG1GHO5DE3X4DvNahZlJOwEkOATvdPc2//hx3mXJ5lFJaF8K8bStd0YGfnOJbMGex21x6c+yfAAOlIPDJzr7cLAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} diff --git a/core/corehttp/gateway/assets/src/style.css b/core/corehttp/gateway/assets/src/style.css deleted file mode 100644 index 3e7b8a734bc..00000000000 --- a/core/corehttp/gateway/assets/src/style.css +++ /dev/null @@ -1,212 +0,0 @@ -body { - color:#34373f; - font-family:"Helvetica Neue", Helvetica, Arial, sans-serif; - font-size:14px; - line-height:1.43; - margin:0; - word-break:break-all; - -webkit-text-size-adjust:100%; - -ms-text-size-adjust:100%; - -webkit-tap-highlight-color:transparent -} - -a { - color:#117eb3; - text-decoration:none -} - -a:hover { - color:#00b0e9; - text-decoration:underline -} - -a:active, -a:visited { - color:#00b0e9 -} - -strong { - font-weight:700 -} - -table { - border-collapse:collapse; - border-spacing:0; - max-width:100%; - width:100% -} - -table:last-child { - border-bottom-left-radius:3px; - border-bottom-right-radius:3px -} - -tr:first-child td { - border-top:0 -} - -tr:nth-of-type(even) { - background-color:#f7f8fa -} - -td { - border-top:1px solid #d9dbe2; - padding:.65em; - vertical-align:top -} - -#page-header { - align-items:center; - background:#0b3a53; - border-bottom:4px solid #69c4cd; - color:#fff; - display:flex; - font-size:1.12em; - font-weight:500; - justify-content:space-between; - padding:0 1em -} - -#page-header a { - color:#69c4cd -} - -#page-header a:active { - color:#9ad4db -} - -#page-header a:hover { - color:#fff -} - -#page-header-logo { - height:2.25em; - margin:.7em .7em .7em 0; - width:7.15em -} - -#page-header-menu { - align-items:center; - display:flex; - margin:.65em 0 -} - -#page-header-menu div { - margin:0 .6em -} - -#page-header-menu div:last-child { - margin:0 0 0 .6em -} - -#page-header-menu svg { - fill:#69c4cd; - height:1.8em; - margin-top:.125em -} - -#page-header-menu svg:hover { - fill:#fff -} - -.menu-item-narrow { - display:none -} - -#content { - border:1px solid #d9dbe2; - border-radius:4px; - margin:1em -} - -#content-header { - background-color:#edf0f4; - border-bottom:1px solid #d9dbe2; - border-top-left-radius:3px; - border-top-right-radius:3px; - padding:.7em 1em -} - -.type-icon, -.type-icon>* { - width:1.15em -} - -.no-linebreak { - white-space:nowrap -} - -.ipfs-hash { - color:#7f8491; - font-family:monospace -} - -@media only screen and (max-width:500px) { - .menu-item-narrow { - display:inline - } - .menu-item-wide { - display:none - } -} - -@media print { - #page-header { - display:none - } - #content-header, - .ipfs-hash, - body { - color:#000 - } - #content-header { - border-bottom:1px solid #000 - } - #content { - border:1px solid #000 - } - a, - a:visited { - color:#000; - text-decoration:underline - } - a[href]:after { - content:" (" attr(href) ")" - } - tr { - page-break-inside:avoid - } - tr:nth-of-type(even) { - background-color:transparent - } - td { - border-top:1px solid #000 - } -} - -@-ms-viewport { - width:device-width -} - -.d-flex { - display:flex -} - -.flex-wrap { - flex-flow:wrap -} - -.flex-shrink-1 { - flex-shrink:1 -} - -.ml-auto { - margin-left:auto -} - -.table-responsive { - display:block; - width:100%; - overflow-x:auto; - -webkit-overflow-scrolling:touch -} diff --git a/core/corehttp/gateway/assets/test/main.go b/core/corehttp/gateway/assets/test/main.go deleted file mode 100644 index dc3c8c46472..00000000000 --- a/core/corehttp/gateway/assets/test/main.go +++ /dev/null @@ -1,126 +0,0 @@ -package main - -import ( - "fmt" - "html/template" - "net/http" - "net/url" - "os" - - "github.com/ipfs/kubo/core/corehttp/gateway/assets" -) - -const ( - directoryTemplateFile = "../directory-index.html" - dagTemplateFile = "../dag-index.html" - - testPath = "/ipfs/QmFooBarQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7/a/b/c" -) - -var directoryTestData = assets.DirectoryTemplateData{ - GatewayURL: "//localhost:3000", - DNSLink: true, - Listing: []assets.DirectoryItem{{ - Size: "25 MiB", - Name: "short-film.mov", - Path: testPath + "/short-film.mov", - Hash: "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", - ShortHash: "QmbW\u2026sMnR", - }, { - Size: "23 KiB", - Name: "250pxيوسف_الوزاني_صورة_ملتقطة_بواسطة_مرصد_هابل_الفضائي_توضح_سديم_السرطان،_وهو_بقايا_مستعر_أعظم._.jpg", - Path: testPath + "/250pxيوسف_الوزاني_صورة_ملتقطة_بواسطة_مرصد_هابل_الفضائي_توضح_سديم_السرطان،_وهو_بقايا_مستعر_أعظم._.jpg", - Hash: "QmUwrKrMTrNv8QjWGKMMH5QV9FMPUtRCoQ6zxTdgxATQW6", - ShortHash: "QmUw\u2026TQW6", - }, { - Size: "1 KiB", - Name: "this-piece-of-papers-got-47-words-37-sentences-58-words-we-wanna-know.txt", - Path: testPath + "/this-piece-of-papers-got-47-words-37-sentences-58-words-we-wanna-know.txt", - Hash: "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", - ShortHash: "bafy\u2026bzdi", - }}, - Size: "25 MiB", - Path: testPath, - Breadcrumbs: []assets.Breadcrumb{{ - Name: "ipfs", - }, { - Name: "QmFooBarQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7", - Path: testPath + "/../../..", - }, { - Name: "a", - Path: testPath + "/../..", - }, { - Name: "b", - Path: testPath + "/..", - }, { - Name: "c", - Path: testPath, - }}, - BackLink: testPath + "/..", - Hash: "QmFooBazBar2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7", -} - -var dagTestData = assets.DagTemplateData{ - Path: "/ipfs/baguqeerabn4wonmz6icnk7dfckuizcsf4e4igua2ohdboecku225xxmujepa", - CID: "baguqeerabn4wonmz6icnk7dfckuizcsf4e4igua2ohdboecku225xxmujepa", - CodecName: "dag-json", - CodecHex: "0x129", -} - -func main() { - mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/dag": - dagTemplate, err := template.New("dag-index.html").ParseFiles(dagTemplateFile) - if err != nil { - http.Error(w, fmt.Sprintf("failed to parse template file: %s", err), http.StatusInternalServerError) - return - } - err = dagTemplate.Execute(w, &dagTestData) - if err != nil { - http.Error(w, fmt.Sprintf("failed to execute template: %s", err), http.StatusInternalServerError) - return - } - case "/directory": - directoryTemplate, err := template.New("directory-index.html").Funcs(template.FuncMap{ - "iconFromExt": func(name string) string { - return "ipfs-_blank" // place-holder - }, - "urlEscape": func(rawUrl string) string { - pathURL := url.URL{Path: rawUrl} - return pathURL.String() - }, - }).ParseFiles(directoryTemplateFile) - if err != nil { - http.Error(w, fmt.Sprintf("failed to parse template file: %s", err), http.StatusInternalServerError) - return - } - err = directoryTemplate.Execute(w, &directoryTestData) - if err != nil { - http.Error(w, fmt.Sprintf("failed to execute template: %s", err), http.StatusInternalServerError) - return - } - case "/": - html := `

Test paths: DAG, Directory.` - _, _ = w.Write([]byte(html)) - default: - http.Redirect(w, r, "/", http.StatusSeeOther) - } - }) - - if _, err := os.Stat(directoryTemplateFile); err != nil { - wd, _ := os.Getwd() - fmt.Printf("could not open template file %q, relative to %q: %s\n", directoryTemplateFile, wd, err) - os.Exit(1) - } - - if _, err := os.Stat(dagTemplateFile); err != nil { - wd, _ := os.Getwd() - fmt.Printf("could not open template file %q, relative to %q: %s\n", dagTemplateFile, wd, err) - os.Exit(1) - } - - fmt.Printf("listening on localhost:3000\n") - _ = http.ListenAndServe("localhost:3000", mux) -} diff --git a/core/corehttp/gateway/gateway.go b/core/corehttp/gateway/gateway.go deleted file mode 100644 index 0882d4fb438..00000000000 --- a/core/corehttp/gateway/gateway.go +++ /dev/null @@ -1,107 +0,0 @@ -package gateway - -import ( - "context" - "net/http" - "sort" - - coreiface "github.com/ipfs/interface-go-ipfs-core" - path "github.com/ipfs/interface-go-ipfs-core/path" -) - -// Config is the configuration that will be applied when creating a new gateway -// handler. -type Config struct { - Headers map[string][]string - Writable bool -} - -// NodeAPI defines the minimal set of API services required by a gateway handler -type NodeAPI interface { - // Unixfs returns an implementation of Unixfs API - Unixfs() coreiface.UnixfsAPI - - // Block returns an implementation of Block API - Block() coreiface.BlockAPI - - // Dag returns an implementation of Dag API - Dag() coreiface.APIDagService - - // Routing returns an implementation of Routing API. - // Used for returning signed IPNS records, see IPIP-0328 - Routing() coreiface.RoutingAPI - - // ResolvePath resolves the path using Unixfs resolver - ResolvePath(context.Context, path.Path) (path.Resolved, error) -} - -// A helper function to clean up a set of headers: -// 1. Canonicalizes. -// 2. Deduplicates. -// 3. Sorts. -func cleanHeaderSet(headers []string) []string { - // Deduplicate and canonicalize. - m := make(map[string]struct{}, len(headers)) - for _, h := range headers { - m[http.CanonicalHeaderKey(h)] = struct{}{} - } - result := make([]string, 0, len(m)) - for k := range m { - result = append(result, k) - } - - // Sort - sort.Strings(result) - return result -} - -// AddAccessControlHeaders adds default headers used for controlling -// cross-origin requests. This function adds several values to the -// Access-Control-Allow-Headers and Access-Control-Expose-Headers entries. -// If the Access-Control-Allow-Origin entry is missing a value of '*' is -// added, indicating that browsers should allow requesting code from any -// origin to access the resource. -// If the Access-Control-Allow-Methods entry is missing a value of 'GET' is -// added, indicating that browsers may use the GET method when issuing cross -// origin requests. -func AddAccessControlHeaders(headers map[string][]string) { - // Hard-coded headers. - const ACAHeadersName = "Access-Control-Allow-Headers" - const ACEHeadersName = "Access-Control-Expose-Headers" - const ACAOriginName = "Access-Control-Allow-Origin" - const ACAMethodsName = "Access-Control-Allow-Methods" - - if _, ok := headers[ACAOriginName]; !ok { - // Default to *all* - headers[ACAOriginName] = []string{"*"} - } - if _, ok := headers[ACAMethodsName]; !ok { - // Default to GET - headers[ACAMethodsName] = []string{http.MethodGet} - } - - headers[ACAHeadersName] = cleanHeaderSet( - append([]string{ - "Content-Type", - "User-Agent", - "Range", - "X-Requested-With", - }, headers[ACAHeadersName]...)) - - headers[ACEHeadersName] = cleanHeaderSet( - append([]string{ - "Content-Length", - "Content-Range", - "X-Chunked-Output", - "X-Stream-Output", - "X-Ipfs-Path", - "X-Ipfs-Roots", - }, headers[ACEHeadersName]...)) -} - -type RequestContextKey string - -const ( - DNSLinkHostnameKey RequestContextKey = "dnslink-hostname" - GatewayHostnameKey RequestContextKey = "gw-hostname" -) diff --git a/core/corehttp/gateway/handler.go b/core/corehttp/gateway/handler.go deleted file mode 100644 index e6354069a6c..00000000000 --- a/core/corehttp/gateway/handler.go +++ /dev/null @@ -1,1126 +0,0 @@ -package gateway - -import ( - "context" - "fmt" - "html/template" - "io" - "mime" - "net/http" - "net/textproto" - "net/url" - "os" - gopath "path" - "regexp" - "runtime/debug" - "strings" - "time" - - cid "github.com/ipfs/go-cid" - ipld "github.com/ipfs/go-ipld-format" - "github.com/ipfs/go-libipfs/files" - logging "github.com/ipfs/go-log" - dag "github.com/ipfs/go-merkledag" - mfs "github.com/ipfs/go-mfs" - path "github.com/ipfs/go-path" - "github.com/ipfs/go-path/resolver" - 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" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "go.uber.org/zap" -) - -var log = logging.Logger("core/server") - -const ( - ipfsPathPrefix = "/ipfs/" - ipnsPathPrefix = "/ipns/" - immutableCacheControl = "public, max-age=29030400, immutable" -) - -var ( - onlyASCII = regexp.MustCompile("[[:^ascii:]]") - noModtime = time.Unix(0, 0) // disables Last-Modified header if passed as modtime -) - -// HTML-based redirect for errors which can be recovered from, but we want -// to provide hint to people that they should fix things on their end. -var redirectTemplate = template.Must(template.New("redirect").Parse(` - - - - - - - -

{{.ErrorMsg}}
(if a redirect does not happen in 10 seconds, use "{{.SuggestedPath}}" instead)
- -`)) - -type redirectTemplateData struct { - RedirectURL string - SuggestedPath string - ErrorMsg string -} - -// handler is a HTTP handler that serves IPFS objects (accessible by default at /ipfs/) -// (it serves requests like GET /ipfs/QmVRzPKPzNtSrEzBFm2UZfxmPAgnaLke4DMcerbsGGSaFe/link) -type handler struct { - config Config - api NodeAPI - offlineAPI NodeAPI - - // generic metrics - firstContentBlockGetMetric *prometheus.HistogramVec - unixfsGetMetric *prometheus.SummaryVec // deprecated, use firstContentBlockGetMetric - - // response type metrics - unixfsFileGetMetric *prometheus.HistogramVec - unixfsGenDirGetMetric *prometheus.HistogramVec - carStreamGetMetric *prometheus.HistogramVec - rawBlockGetMetric *prometheus.HistogramVec -} - -// StatusResponseWriter enables us to override HTTP Status Code passed to -// WriteHeader function inside of http.ServeContent. Decision is based on -// presence of HTTP Headers such as Location. -type statusResponseWriter struct { - http.ResponseWriter -} - -// Custom type for collecting error details to be handled by `webRequestError` -type requestError struct { - Message string - StatusCode int - Err error -} - -func (r *requestError) Error() string { - return r.Err.Error() -} - -func newRequestError(message string, err error, statusCode int) *requestError { - return &requestError{ - Message: message, - Err: err, - StatusCode: statusCode, - } -} - -func (sw *statusResponseWriter) WriteHeader(code int) { - // Check if we need to adjust Status Code to account for scheduled redirect - // This enables us to return payload along with HTTP 301 - // for subdomain redirect in web browsers while also returning body for cli - // tools which do not follow redirects by default (curl, wget). - redirect := sw.ResponseWriter.Header().Get("Location") - if redirect != "" && code == http.StatusOK { - code = http.StatusMovedPermanently - log.Debugw("subdomain redirect", "location", redirect, "status", code) - } - sw.ResponseWriter.WriteHeader(code) -} - -// ServeContent replies to the request using the content in the provided ReadSeeker -// and returns the status code written and any error encountered during a write. -// It wraps http.ServeContent which takes care of If-None-Match+Etag, -// Content-Length and range requests. -func ServeContent(w http.ResponseWriter, req *http.Request, name string, modtime time.Time, content io.ReadSeeker) (int, bool, error) { - ew := &errRecordingResponseWriter{ResponseWriter: w} - http.ServeContent(ew, req, name, modtime, content) - - // When we calculate some metrics we want a flag that lets us to ignore - // errors and 304 Not Modified, and only care when requested data - // was sent in full. - dataSent := ew.code/100 == 2 && ew.err == nil - - return ew.code, dataSent, ew.err -} - -// errRecordingResponseWriter wraps a ResponseWriter to record the status code and any write error. -type errRecordingResponseWriter struct { - http.ResponseWriter - code int - err error -} - -func (w *errRecordingResponseWriter) WriteHeader(code int) { - if w.code == 0 { - w.code = code - } - w.ResponseWriter.WriteHeader(code) -} - -func (w *errRecordingResponseWriter) Write(p []byte) (int, error) { - n, err := w.ResponseWriter.Write(p) - if err != nil && w.err == nil { - w.err = err - } - return n, err -} - -// ReadFrom exposes errRecordingResponseWriter's underlying ResponseWriter to io.Copy -// to allow optimized methods to be taken advantage of. -func (w *errRecordingResponseWriter) ReadFrom(r io.Reader) (n int64, err error) { - n, err = io.Copy(w.ResponseWriter, r) - if err != nil && w.err == nil { - w.err = err - } - return n, err -} - -func newSummaryMetric(name string, help string) *prometheus.SummaryVec { - summaryMetric := prometheus.NewSummaryVec( - prometheus.SummaryOpts{ - Namespace: "ipfs", - Subsystem: "http", - Name: name, - Help: help, - }, - []string{"gateway"}, - ) - if err := prometheus.Register(summaryMetric); err != nil { - if are, ok := err.(prometheus.AlreadyRegisteredError); ok { - summaryMetric = are.ExistingCollector.(*prometheus.SummaryVec) - } else { - log.Errorf("failed to register ipfs_http_%s: %v", name, err) - } - } - return summaryMetric -} - -func newHistogramMetric(name string, help string) *prometheus.HistogramVec { - // We can add buckets as a parameter in the future, but for now using static defaults - // suggested in https://github.com/ipfs/kubo/issues/8441 - defaultBuckets := []float64{0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 30, 60} - histogramMetric := prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Namespace: "ipfs", - Subsystem: "http", - Name: name, - Help: help, - Buckets: defaultBuckets, - }, - []string{"gateway"}, - ) - if err := prometheus.Register(histogramMetric); err != nil { - if are, ok := err.(prometheus.AlreadyRegisteredError); ok { - histogramMetric = are.ExistingCollector.(*prometheus.HistogramVec) - } else { - log.Errorf("failed to register ipfs_http_%s: %v", name, err) - } - } - return histogramMetric -} - -// NewHandler returns an http.Handler that can act as a gateway to IPFS content -// offlineApi is a version of the API that should not make network requests for missing data -func NewHandler(c Config, api NodeAPI, offlineAPI NodeAPI) http.Handler { - return newHandler(c, api, offlineAPI) -} - -func newHandler(c Config, api NodeAPI, offlineAPI NodeAPI) *handler { - i := &handler{ - config: c, - api: api, - offlineAPI: offlineAPI, - // Improved Metrics - // ---------------------------- - // Time till the first content block (bar in /ipfs/cid/foo/bar) - // (format-agnostic, across all response types) - firstContentBlockGetMetric: newHistogramMetric( - "gw_first_content_block_get_latency_seconds", - "The time till the first content block is received on GET from the gateway.", - ), - - // Response-type specific metrics - // ---------------------------- - // UnixFS: time it takes to return a file - unixfsFileGetMetric: newHistogramMetric( - "gw_unixfs_file_get_duration_seconds", - "The time to serve an entire UnixFS file from the gateway.", - ), - // UnixFS: time it takes to generate static HTML with directory listing - unixfsGenDirGetMetric: newHistogramMetric( - "gw_unixfs_gen_dir_listing_get_duration_seconds", - "The time to serve a generated UnixFS HTML directory listing from the gateway.", - ), - // CAR: time it takes to return requested CAR stream - carStreamGetMetric: newHistogramMetric( - "gw_car_stream_get_duration_seconds", - "The time to GET an entire CAR stream from the gateway.", - ), - // Block: time it takes to return requested Block - rawBlockGetMetric: newHistogramMetric( - "gw_raw_block_get_duration_seconds", - "The time to GET an entire raw Block from the gateway.", - ), - - // Legacy Metrics - // ---------------------------- - unixfsGetMetric: newSummaryMetric( // TODO: remove? - // (deprecated, use firstContentBlockGetMetric instead) - "unixfs_get_latency_seconds", - "The time to receive the first UnixFS node on a GET from the gateway.", - ), - } - return i -} - -func parseIpfsPath(p string) (cid.Cid, string, error) { - rootPath, err := path.ParsePath(p) - if err != nil { - return cid.Cid{}, "", err - } - - // Check the path. - rsegs := rootPath.Segments() - if rsegs[0] != "ipfs" { - return cid.Cid{}, "", fmt.Errorf("WritableGateway: only ipfs paths supported") - } - - rootCid, err := cid.Decode(rsegs[1]) - if err != nil { - return cid.Cid{}, "", err - } - - return rootCid, path.Join(rsegs[2:]), nil -} - -func (i *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // the hour is a hard fallback, we don't expect it to happen, but just in case - ctx, cancel := context.WithTimeout(r.Context(), time.Hour) - defer cancel() - r = r.WithContext(ctx) - - defer func() { - if r := recover(); r != nil { - log.Error("A panic occurred in the gateway handler!") - log.Error(r) - debug.PrintStack() - } - }() - - if i.config.Writable { - switch r.Method { - case http.MethodPost: - i.postHandler(w, r) - return - case http.MethodPut: - i.putHandler(w, r) - return - case http.MethodDelete: - i.deleteHandler(w, r) - return - } - } - - switch r.Method { - case http.MethodGet, http.MethodHead: - i.getOrHeadHandler(w, r) - return - case http.MethodOptions: - i.optionsHandler(w, r) - return - } - - errmsg := "Method " + r.Method + " not allowed: " - var status int - if !i.config.Writable { - status = http.StatusMethodNotAllowed - errmsg = errmsg + "read only access" - w.Header().Add("Allow", http.MethodGet) - w.Header().Add("Allow", http.MethodHead) - w.Header().Add("Allow", http.MethodOptions) - } else { - status = http.StatusBadRequest - errmsg = errmsg + "bad request for " + r.URL.Path - } - http.Error(w, errmsg, status) -} - -func (i *handler) optionsHandler(w http.ResponseWriter, r *http.Request) { - /* - OPTIONS is a noop request that is used by the browsers to check - if server accepts cross-site XMLHttpRequest (indicated by the presence of CORS headers) - https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests - */ - i.addUserHeaders(w) // return all custom headers (including CORS ones, if set) -} - -func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { - begin := time.Now() - - logger := log.With("from", r.RequestURI) - logger.Debug("http request received") - - if err := handleUnsupportedHeaders(r); err != nil { - webRequestError(w, err) - return - } - - if requestHandled := handleProtocolHandlerRedirect(w, r, logger); requestHandled { - return - } - - if err := handleServiceWorkerRegistration(r); err != nil { - webRequestError(w, err) - return - } - - contentPath := ipath.New(r.URL.Path) - - if requestHandled := i.handleOnlyIfCached(w, r, contentPath, logger); requestHandled { - return - } - - if requestHandled := handleSuperfluousNamespace(w, r, contentPath); requestHandled { - return - } - - // Detect when explicit Accept header or ?format parameter are present - responseFormat, formatParams, err := customResponseFormat(r) - if err != nil { - webError(w, "error while processing the Accept header", err, http.StatusBadRequest) - return - } - trace.SpanFromContext(r.Context()).SetAttributes(attribute.String("ResponseFormat", responseFormat)) - - resolvedPath, contentPath, ok := i.handlePathResolution(w, r, responseFormat, contentPath, logger) - if !ok { - return - } - trace.SpanFromContext(r.Context()).SetAttributes(attribute.String("ResolvedPath", resolvedPath.String())) - - // Detect when If-None-Match HTTP header allows returning HTTP 304 Not Modified - if inm := r.Header.Get("If-None-Match"); inm != "" { - pathCid := resolvedPath.Cid() - // need to check against both File and Dir Etag variants - // because this inexpensive check happens before we do any I/O - cidEtag := getEtag(r, pathCid) - dirEtag := getDirListingEtag(pathCid) - if etagMatch(inm, cidEtag, dirEtag) { - // Finish early if client already has a matching Etag - w.WriteHeader(http.StatusNotModified) - return - } - } - - if err := i.handleGettingFirstBlock(r, begin, contentPath, resolvedPath); err != nil { - webRequestError(w, err) - return - } - - if err := i.setCommonHeaders(w, r, contentPath); err != nil { - webRequestError(w, err) - return - } - - // Support custom response formats passed via ?format or Accept HTTP header - switch responseFormat { - case "", "application/json", "application/cbor": - switch mc.Code(resolvedPath.Cid().Prefix().Codec) { - case mc.Json, mc.DagJson, mc.Cbor, 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) - i.serveRawBlock(r.Context(), w, r, resolvedPath, contentPath, begin) - return - case "application/vnd.ipld.car": - logger.Debugw("serving car stream", "path", contentPath) - carVersion := formatParams["version"] - i.serveCAR(r.Context(), w, r, resolvedPath, contentPath, carVersion, begin) - return - case "application/x-tar": - logger.Debugw("serving tar file", "path", contentPath) - i.serveTAR(r.Context(), w, r, resolvedPath, contentPath, begin, logger) - return - case "application/vnd.ipld.dag-json", "application/vnd.ipld.dag-cbor": - logger.Debugw("serving codec", "path", contentPath) - i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat) - case "application/vnd.ipfs.ipns-record": - logger.Debugw("serving ipns record", "path", contentPath) - i.serveIpnsRecord(r.Context(), w, r, resolvedPath, contentPath, begin, logger) - return - default: // catch-all for unsuported application/vnd.* - err := fmt.Errorf("unsupported format %q", responseFormat) - webError(w, "failed to respond with requested content type", err, http.StatusBadRequest) - return - } -} - -func (i *handler) postHandler(w http.ResponseWriter, r *http.Request) { - p, err := i.api.Unixfs().Add(r.Context(), files.NewReaderFile(r.Body)) - if err != nil { - internalWebError(w, err) - return - } - - i.addUserHeaders(w) // ok, _now_ write user's headers. - w.Header().Set("IPFS-Hash", p.Cid().String()) - log.Debugw("CID created, http redirect", "from", r.URL, "to", p, "status", http.StatusCreated) - http.Redirect(w, r, p.String(), http.StatusCreated) -} - -func (i *handler) putHandler(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - ds := i.api.Dag() - - // Parse the path - rootCid, newPath, err := parseIpfsPath(r.URL.Path) - if err != nil { - webError(w, "WritableGateway: failed to parse the path", err, http.StatusBadRequest) - return - } - if newPath == "" || newPath == "/" { - http.Error(w, "WritableGateway: empty path", http.StatusBadRequest) - return - } - newDirectory, newFileName := gopath.Split(newPath) - - // Resolve the old root. - - rnode, err := ds.Get(ctx, rootCid) - if err != nil { - webError(w, "WritableGateway: Could not create DAG from request", err, http.StatusInternalServerError) - return - } - - pbnd, ok := rnode.(*dag.ProtoNode) - if !ok { - webError(w, "Cannot read non protobuf nodes through gateway", dag.ErrNotProtobuf, http.StatusBadRequest) - return - } - - // Create the new file. - newFilePath, err := i.api.Unixfs().Add(ctx, files.NewReaderFile(r.Body)) - if err != nil { - webError(w, "WritableGateway: could not create DAG from request", err, http.StatusInternalServerError) - return - } - - newFile, err := ds.Get(ctx, newFilePath.Cid()) - if err != nil { - webError(w, "WritableGateway: failed to resolve new file", err, http.StatusInternalServerError) - return - } - - // Patch the new file into the old root. - - root, err := mfs.NewRoot(ctx, ds, pbnd, nil) - if err != nil { - webError(w, "WritableGateway: failed to create MFS root", err, http.StatusBadRequest) - return - } - - if newDirectory != "" { - err := mfs.Mkdir(root, newDirectory, mfs.MkdirOpts{Mkparents: true, Flush: false}) - if err != nil { - webError(w, "WritableGateway: failed to create MFS directory", err, http.StatusInternalServerError) - return - } - } - dirNode, err := mfs.Lookup(root, newDirectory) - if err != nil { - webError(w, "WritableGateway: failed to lookup directory", err, http.StatusInternalServerError) - return - } - dir, ok := dirNode.(*mfs.Directory) - if !ok { - http.Error(w, "WritableGateway: target directory is not a directory", http.StatusBadRequest) - return - } - err = dir.Unlink(newFileName) - switch err { - case os.ErrNotExist, nil: - default: - webError(w, "WritableGateway: failed to replace existing file", err, http.StatusBadRequest) - return - } - err = dir.AddChild(newFileName, newFile) - if err != nil { - webError(w, "WritableGateway: failed to link file into directory", err, http.StatusInternalServerError) - return - } - nnode, err := root.GetDirectory().GetNode() - if err != nil { - webError(w, "WritableGateway: failed to finalize", err, http.StatusInternalServerError) - return - } - newcid := nnode.Cid() - - i.addUserHeaders(w) // ok, _now_ write user's headers. - w.Header().Set("IPFS-Hash", newcid.String()) - - redirectURL := gopath.Join(ipfsPathPrefix, newcid.String(), newPath) - log.Debugw("CID replaced, redirect", "from", r.URL, "to", redirectURL, "status", http.StatusCreated) - http.Redirect(w, r, redirectURL, http.StatusCreated) -} - -func (i *handler) deleteHandler(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - // parse the path - - rootCid, newPath, err := parseIpfsPath(r.URL.Path) - if err != nil { - webError(w, "WritableGateway: failed to parse the path", err, http.StatusBadRequest) - return - } - if newPath == "" || newPath == "/" { - http.Error(w, "WritableGateway: empty path", http.StatusBadRequest) - return - } - directory, filename := gopath.Split(newPath) - - // lookup the root - - rootNodeIPLD, err := i.api.Dag().Get(ctx, rootCid) - if err != nil { - webError(w, "WritableGateway: failed to resolve root CID", err, http.StatusInternalServerError) - return - } - rootNode, ok := rootNodeIPLD.(*dag.ProtoNode) - if !ok { - http.Error(w, "WritableGateway: empty path", http.StatusInternalServerError) - return - } - - // construct the mfs root - - root, err := mfs.NewRoot(ctx, i.api.Dag(), rootNode, nil) - if err != nil { - webError(w, "WritableGateway: failed to construct the MFS root", err, http.StatusBadRequest) - return - } - - // lookup the parent directory - - parentNode, err := mfs.Lookup(root, directory) - if err != nil { - webError(w, "WritableGateway: failed to look up parent", err, http.StatusInternalServerError) - return - } - - parent, ok := parentNode.(*mfs.Directory) - if !ok { - http.Error(w, "WritableGateway: parent is not a directory", http.StatusInternalServerError) - return - } - - // delete the file - - switch parent.Unlink(filename) { - case nil, os.ErrNotExist: - default: - webError(w, "WritableGateway: failed to remove file", err, http.StatusInternalServerError) - return - } - - nnode, err := root.GetDirectory().GetNode() - if err != nil { - webError(w, "WritableGateway: failed to finalize", err, http.StatusInternalServerError) - return - } - ncid := nnode.Cid() - - i.addUserHeaders(w) // ok, _now_ write user's headers. - w.Header().Set("IPFS-Hash", ncid.String()) - - redirectURL := gopath.Join(ipfsPathPrefix+ncid.String(), directory) - // note: StatusCreated is technically correct here as we created a new resource. - log.Debugw("CID deleted, redirect", "from", r.RequestURI, "to", redirectURL, "status", http.StatusCreated) - http.Redirect(w, r, redirectURL, http.StatusCreated) -} - -func (i *handler) addUserHeaders(w http.ResponseWriter) { - for k, v := range i.config.Headers { - w.Header()[k] = v - } -} - -func addCacheControlHeaders(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, fileCid cid.Cid) (modtime time.Time) { - // Set Etag to based on CID (override whatever was set before) - w.Header().Set("Etag", getEtag(r, fileCid)) - - // Set Cache-Control and Last-Modified based on contentPath properties - if contentPath.Mutable() { - // mutable namespaces such as /ipns/ can't be cached forever - - /* For now we set Last-Modified to Now() to leverage caching heuristics built into modern browsers: - * https://github.com/ipfs/kubo/pull/8074#pullrequestreview-645196768 - * but we should not set it to fake values and use Cache-Control based on TTL instead */ - modtime = time.Now() - - // TODO: set Cache-Control based on TTL of IPNS/DNSLink: https://github.com/ipfs/kubo/issues/1818#issuecomment-1015849462 - // TODO: set Last-Modified based on /ipns/ publishing timestamp? - } else { - // immutable! CACHE ALL THE THINGS, FOREVER! wolololol - w.Header().Set("Cache-Control", immutableCacheControl) - - // Set modtime to 'zero time' to disable Last-Modified header (superseded by Cache-Control) - modtime = noModtime - - // TODO: set Last-Modified? - TBD - /ipfs/ modification metadata is present in unixfs 1.5 https://github.com/ipfs/kubo/issues/6920? - } - - return modtime -} - -// Set Content-Disposition if filename URL query param is present, return preferred filename -func addContentDispositionHeader(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) string { - /* This logic enables: - * - creation of HTML links that trigger "Save As.." dialog instead of being rendered by the browser - * - overriding the filename used when saving subresource assets on HTML page - * - providing a default filename for HTTP clients when downloading direct /ipfs/CID without any subpath - */ - - // URL param ?filename=cat.jpg triggers Content-Disposition: [..] filename - // which impacts default name used in "Save As.." dialog - name := getFilename(contentPath) - urlFilename := r.URL.Query().Get("filename") - if urlFilename != "" { - disposition := "inline" - // URL param ?download=true triggers Content-Disposition: [..] attachment - // which skips rendering and forces "Save As.." dialog in browsers - if r.URL.Query().Get("download") == "true" { - disposition = "attachment" - } - setContentDispositionHeader(w, urlFilename, disposition) - name = urlFilename - } - return name -} - -// Set Content-Disposition to arbitrary filename and disposition -func setContentDispositionHeader(w http.ResponseWriter, filename string, disposition string) { - utf8Name := url.PathEscape(filename) - asciiName := url.PathEscape(onlyASCII.ReplaceAllLiteralString(filename, "_")) - w.Header().Set("Content-Disposition", fmt.Sprintf("%s; filename=\"%s\"; filename*=UTF-8''%s", disposition, asciiName, utf8Name)) -} - -// Set X-Ipfs-Roots with logical CID array for efficient HTTP cache invalidation. -func (i *handler) buildIpfsRootsHeader(contentPath string, r *http.Request) (string, error) { - /* - These are logical roots where each CID represent one path segment - and resolves to either a directory or the root block of a file. - The main purpose of this header is allow HTTP caches to do smarter decisions - around cache invalidation (eg. keep specific subdirectory/file if it did not change) - - A good example is Wikipedia, which is HAMT-sharded, but we only care about - logical roots that represent each segment of the human-readable content - path: - - Given contentPath = /ipns/en.wikipedia-on-ipfs.org/wiki/Block_of_Wikipedia_in_Turkey - rootCidList is a generated by doing `ipfs resolve -r` on each sub path: - /ipns/en.wikipedia-on-ipfs.org → bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze - /ipns/en.wikipedia-on-ipfs.org/wiki/ → bafybeihn2f7lhumh4grizksi2fl233cyszqadkn424ptjajfenykpsaiw4 - /ipns/en.wikipedia-on-ipfs.org/wiki/Block_of_Wikipedia_in_Turkey → bafkreibn6euazfvoghepcm4efzqx5l3hieof2frhp254hio5y7n3hv5rma - - The result is an ordered array of values: - X-Ipfs-Roots: bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze,bafybeihn2f7lhumh4grizksi2fl233cyszqadkn424ptjajfenykpsaiw4,bafkreibn6euazfvoghepcm4efzqx5l3hieof2frhp254hio5y7n3hv5rma - - Note that while the top one will change every time any article is changed, - the last root (responsible for specific article) may not change at all. - */ - var sp strings.Builder - var pathRoots []string - pathSegments := strings.Split(contentPath[6:], "/") - sp.WriteString(contentPath[:5]) // /ipfs or /ipns - for _, root := range pathSegments { - if root == "" { - continue - } - sp.WriteString("/") - sp.WriteString(root) - resolvedSubPath, err := i.api.ResolvePath(r.Context(), ipath.New(sp.String())) - if err != nil { - return "", err - } - pathRoots = append(pathRoots, resolvedSubPath.Cid().String()) - } - rootCidList := strings.Join(pathRoots, ",") // convention from rfc2616#sec4.2 - return rootCidList, nil -} - -func webRequestError(w http.ResponseWriter, err *requestError) { - webError(w, err.Message, err.Err, err.StatusCode) -} - -func webError(w http.ResponseWriter, message string, err error, defaultCode int) { - if _, ok := err.(resolver.ErrNoLink); ok { - webErrorWithCode(w, message, err, http.StatusNotFound) - } else if err == routing.ErrNotFound { - webErrorWithCode(w, message, err, http.StatusNotFound) - } else if ipld.IsNotFound(err) { - webErrorWithCode(w, message, err, http.StatusNotFound) - } else if err == context.DeadlineExceeded { - webErrorWithCode(w, message, err, http.StatusRequestTimeout) - } else { - webErrorWithCode(w, message, err, defaultCode) - } -} - -func webErrorWithCode(w http.ResponseWriter, message string, err error, code int) { - http.Error(w, fmt.Sprintf("%s: %s", message, err), code) - if code >= 500 { - log.Warnf("server error: %s: %s", message, err) - } -} - -// return a 500 error and log -func internalWebError(w http.ResponseWriter, err error) { - webErrorWithCode(w, "internalWebError", err, http.StatusInternalServerError) -} - -func getFilename(contentPath ipath.Path) string { - s := contentPath.String() - if (strings.HasPrefix(s, ipfsPathPrefix) || strings.HasPrefix(s, ipnsPathPrefix)) && strings.Count(gopath.Clean(s), "/") <= 2 { - // Don't want to treat ipfs.io in /ipns/ipfs.io as a filename. - return "" - } - return gopath.Base(s) -} - -// etagMatch evaluates if we can respond with HTTP 304 Not Modified -// It supports multiple weak and strong etags passed in If-None-Matc stringh -// including the wildcard one. -func etagMatch(ifNoneMatchHeader string, cidEtag string, dirEtag string) bool { - buf := ifNoneMatchHeader - for { - buf = textproto.TrimString(buf) - if len(buf) == 0 { - break - } - if buf[0] == ',' { - buf = buf[1:] - continue - } - // If-None-Match: * should match against any etag - if buf[0] == '*' { - return true - } - etag, remain := scanETag(buf) - if etag == "" { - break - } - // Check for match both strong and weak etags - if etagWeakMatch(etag, cidEtag) || etagWeakMatch(etag, dirEtag) { - return true - } - buf = remain - } - return false -} - -// scanETag determines if a syntactically valid ETag is present at s. If so, -// the ETag and remaining text after consuming ETag is returned. Otherwise, -// it returns "", "". -// (This is the same logic as one executed inside of http.ServeContent) -func scanETag(s string) (etag string, remain string) { - s = textproto.TrimString(s) - start := 0 - if strings.HasPrefix(s, "W/") { - start = 2 - } - if len(s[start:]) < 2 || s[start] != '"' { - return "", "" - } - // ETag is either W/"text" or "text". - // See RFC 7232 2.3. - for i := start + 1; i < len(s); i++ { - c := s[i] - switch { - // Character values allowed in ETags. - case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80: - case c == '"': - return s[:i+1], s[i+1:] - default: - return "", "" - } - } - return "", "" -} - -// etagWeakMatch reports whether a and b match using weak ETag comparison. -func etagWeakMatch(a, b string) bool { - return strings.TrimPrefix(a, "W/") == strings.TrimPrefix(b, "W/") -} - -// generate Etag value based on HTTP request and CID -func getEtag(r *http.Request, cid cid.Cid) string { - prefix := `"` - suffix := `"` - responseFormat, _, err := customResponseFormat(r) - if err == nil && responseFormat != "" { - // application/vnd.ipld.foo → foo - // application/x-bar → x-bar - shortFormat := responseFormat[strings.LastIndexAny(responseFormat, "/.")+1:] - // Etag: "cid.shortFmt" (gives us nice compression together with Content-Disposition in block (raw) and car responses) - suffix = `.` + shortFormat + suffix - } - // TODO: include selector suffix when https://github.com/ipfs/kubo/issues/8769 lands - return prefix + cid.String() + suffix -} - -// return explicit response format if specified in request as query parameter or via Accept HTTP header -func customResponseFormat(r *http.Request) (mediaType string, params map[string]string, err error) { - if formatParam := r.URL.Query().Get("format"); formatParam != "" { - // translate query param to a content type - switch formatParam { - case "raw": - return "application/vnd.ipld.raw", nil, nil - case "car": - return "application/vnd.ipld.car", nil, nil - case "tar": - return "application/x-tar", nil, nil - case "json": - return "application/json", nil, nil - case "cbor": - return "application/cbor", nil, nil - case "dag-json": - return "application/vnd.ipld.dag-json", nil, nil - case "dag-cbor": - return "application/vnd.ipld.dag-cbor", nil, nil - case "ipns-record": - return "application/vnd.ipfs.ipns-record", nil, nil - } - } - // Browsers and other user agents will send Accept header with generic types like: - // Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 - // We only care about explicit, vendor-specific content-types and respond to the first match (in order). - // TODO: make this RFC compliant and respect weights (eg. return CAR for Accept:application/vnd.ipld.dag-json;q=0.1,application/vnd.ipld.car;q=0.2) - for _, header := range r.Header.Values("Accept") { - for _, value := range strings.Split(header, ",") { - accept := strings.TrimSpace(value) - // respond to the very first matching content type - if strings.HasPrefix(accept, "application/vnd.ipld") || - strings.HasPrefix(accept, "application/x-tar") || - strings.HasPrefix(accept, "application/json") || - strings.HasPrefix(accept, "application/cbor") || - strings.HasPrefix(accept, "application/vnd.ipfs") { - mediatype, params, err := mime.ParseMediaType(accept) - if err != nil { - return "", nil, err - } - return mediatype, params, nil - } - } - } - // If none of special-cased content types is found, return empty string - // to indicate default, implicit UnixFS response should be prepared - return "", nil, nil -} - -// returns unquoted path with all special characters revealed as \u codes -func debugStr(path string) string { - q := fmt.Sprintf("%+q", path) - if len(q) >= 3 { - q = q[1 : len(q)-1] - } - return q -} - -// Resolve the provided contentPath including any special handling related to -// the requested responseFormat. Returned ok flag indicates if gateway handler -// should continue processing the request. -func (i *handler) handlePathResolution(w http.ResponseWriter, r *http.Request, responseFormat string, contentPath ipath.Path, logger *zap.SugaredLogger) (resolvedPath ipath.Resolved, newContentPath ipath.Path, ok bool) { - // Attempt to resolve the provided path. - resolvedPath, err := i.api.ResolvePath(r.Context(), contentPath) - - switch err { - case nil: - return resolvedPath, contentPath, true - case coreiface.ErrOffline: - webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusServiceUnavailable) - return nil, nil, false - default: - // The path can't be resolved. - if isUnixfsResponseFormat(responseFormat) { - // If we have origin isolation (subdomain gw, DNSLink website), - // and response type is UnixFS (default for website hosting) - // check for presence of _redirects file and apply rules defined there. - // See: https://github.com/ipfs/specs/pull/290 - if hasOriginIsolation(r) { - resolvedPath, newContentPath, ok, hadMatchingRule := i.serveRedirectsIfPresent(w, r, resolvedPath, contentPath, logger) - if hadMatchingRule { - logger.Debugw("applied a rule from _redirects file") - return resolvedPath, newContentPath, ok - } - } - - // if Accept is text/html, see if ipfs-404.html is present - // This logic isn't documented and will likely be removed at some point. - // Any 404 logic in _redirects above will have already run by this time, so it's really an extra fall back - if i.serveLegacy404IfPresent(w, r, contentPath) { - logger.Debugw("served legacy 404") - return nil, nil, false - } - } - - // Note: webError will replace http.StatusBadRequest with StatusNotFound if necessary - webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusBadRequest) - return nil, nil, false - } -} - -// Detect 'Cache-Control: only-if-cached' in request and return data if it is already in the local datastore. -// https://github.com/ipfs/specs/blob/main/http-gateways/PATH_GATEWAY.md#cache-control-request-header -func (i *handler) handleOnlyIfCached(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, logger *zap.SugaredLogger) (requestHandled bool) { - if r.Header.Get("Cache-Control") == "only-if-cached" { - _, err := i.offlineAPI.Block().Stat(r.Context(), contentPath) - if err != nil { - if r.Method == http.MethodHead { - w.WriteHeader(http.StatusPreconditionFailed) - return true - } - errMsg := fmt.Sprintf("%q not in local datastore", contentPath.String()) - http.Error(w, errMsg, http.StatusPreconditionFailed) - return true - } - if r.Method == http.MethodHead { - w.WriteHeader(http.StatusOK) - return true - } - } - return false -} - -func handleUnsupportedHeaders(r *http.Request) (err *requestError) { - // X-Ipfs-Gateway-Prefix was removed (https://github.com/ipfs/kubo/issues/7702) - // TODO: remove this after go-ipfs 0.13 ships - if prfx := r.Header.Get("X-Ipfs-Gateway-Prefix"); prfx != "" { - err := fmt.Errorf("X-Ipfs-Gateway-Prefix support was removed: https://github.com/ipfs/kubo/issues/7702") - return newRequestError("unsupported HTTP header", err, http.StatusBadRequest) - } - return nil -} - -// ?uri query param support for requests produced by web browsers -// via navigator.registerProtocolHandler Web API -// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler -// TLDR: redirect /ipfs/?uri=ipfs%3A%2F%2Fcid%3Fquery%3Dval to /ipfs/cid?query=val -func handleProtocolHandlerRedirect(w http.ResponseWriter, r *http.Request, logger *zap.SugaredLogger) (requestHandled bool) { - if uriParam := r.URL.Query().Get("uri"); uriParam != "" { - u, err := url.Parse(uriParam) - if err != nil { - webError(w, "failed to parse uri query parameter", err, http.StatusBadRequest) - return true - } - if u.Scheme != "ipfs" && u.Scheme != "ipns" { - webError(w, "uri query parameter scheme must be ipfs or ipns", err, http.StatusBadRequest) - return true - } - path := u.Path - if u.RawQuery != "" { // preserve query if present - path = path + "?" + u.RawQuery - } - - redirectURL := gopath.Join("/", u.Scheme, u.Host, path) - logger.Debugw("uri param, redirect", "to", redirectURL, "status", http.StatusMovedPermanently) - http.Redirect(w, r, redirectURL, http.StatusMovedPermanently) - return true - } - - return false -} - -// Disallow Service Worker registration on namespace roots -// https://github.com/ipfs/kubo/issues/4025 -func handleServiceWorkerRegistration(r *http.Request) (err *requestError) { - if r.Header.Get("Service-Worker") == "script" { - matched, _ := regexp.MatchString(`^/ip[fn]s/[^/]+$`, r.URL.Path) - if matched { - err := fmt.Errorf("registration is not allowed for this scope") - return newRequestError("navigator.serviceWorker", err, http.StatusBadRequest) - } - } - - return nil -} - -// Attempt to fix redundant /ipfs/ namespace as long as resulting -// 'intended' path is valid. This is in case gremlins were tickled -// wrong way and user ended up at /ipfs/ipfs/{cid} or /ipfs/ipns/{id} -// like in bafybeien3m7mdn6imm425vc2s22erzyhbvk5n3ofzgikkhmdkh5cuqbpbq :^)) -func handleSuperfluousNamespace(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) (requestHandled bool) { - // If the path is valid, there's nothing to do - if pathErr := contentPath.IsValid(); pathErr == nil { - return false - } - - // If there's no superflous namespace, there's nothing to do - if !(strings.HasPrefix(r.URL.Path, "/ipfs/ipfs/") || strings.HasPrefix(r.URL.Path, "/ipfs/ipns/")) { - return false - } - - // Attempt to fix the superflous namespace - intendedPath := ipath.New(strings.TrimPrefix(r.URL.Path, "/ipfs")) - if err := intendedPath.IsValid(); err != nil { - webError(w, "invalid ipfs path", err, http.StatusBadRequest) - return true - } - intendedURL := intendedPath.String() - if r.URL.RawQuery != "" { - // we render HTML, so ensure query entries are properly escaped - q, _ := url.ParseQuery(r.URL.RawQuery) - intendedURL = intendedURL + "?" + q.Encode() - } - // return HTTP 400 (Bad Request) with HTML error page that: - // - points at correct canonical path via header - // - displays human-readable error - // - redirects to intendedURL after a short delay - - w.WriteHeader(http.StatusBadRequest) - if err := redirectTemplate.Execute(w, redirectTemplateData{ - RedirectURL: intendedURL, - SuggestedPath: intendedPath.String(), - ErrorMsg: fmt.Sprintf("invalid path: %q should be %q", r.URL.Path, intendedPath.String()), - }); err != nil { - webError(w, "failed to redirect when fixing superfluous namespace", err, http.StatusBadRequest) - } - - return true -} - -func (i *handler) handleGettingFirstBlock(r *http.Request, begin time.Time, contentPath ipath.Path, resolvedPath ipath.Resolved) *requestError { - // Update the global metric of the time it takes to read the final root block of the requested resource - // NOTE: for legacy reasons this happens before we go into content-type specific code paths - _, err := i.api.Block().Get(r.Context(), resolvedPath) - if err != nil { - return newRequestError("ipfs block get "+resolvedPath.Cid().String(), err, http.StatusInternalServerError) - } - ns := contentPath.Namespace() - timeToGetFirstContentBlock := time.Since(begin).Seconds() - i.unixfsGetMetric.WithLabelValues(ns).Observe(timeToGetFirstContentBlock) // deprecated, use firstContentBlockGetMetric instead - i.firstContentBlockGetMetric.WithLabelValues(ns).Observe(timeToGetFirstContentBlock) - return nil -} - -func (i *handler) setCommonHeaders(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) *requestError { - i.addUserHeaders(w) // ok, _now_ write user's headers. - w.Header().Set("X-Ipfs-Path", contentPath.String()) - - if rootCids, err := i.buildIpfsRootsHeader(contentPath.String(), r); err == nil { - w.Header().Set("X-Ipfs-Roots", rootCids) - } else { // this should never happen, as we resolved the contentPath already - return newRequestError("error while resolving X-Ipfs-Roots", err, http.StatusInternalServerError) - } - - return nil -} - -// spanTrace starts a new span using the standard IPFS tracing conventions. -func spanTrace(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { - return otel.Tracer("go-libipfs").Start(ctx, fmt.Sprintf("%s.%s", " Gateway", spanName), opts...) -} diff --git a/core/corehttp/gateway/handler_block.go b/core/corehttp/gateway/handler_block.go deleted file mode 100644 index 23a22f44778..00000000000 --- a/core/corehttp/gateway/handler_block.go +++ /dev/null @@ -1,54 +0,0 @@ -package gateway - -import ( - "bytes" - "context" - "io" - "net/http" - "time" - - ipath "github.com/ipfs/interface-go-ipfs-core/path" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -// serveRawBlock returns bytes behind a raw block -func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time) { - ctx, span := spanTrace(ctx, "ServeRawBlock", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) - defer span.End() - 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) - - // Set Content-Disposition - var name string - if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" { - name = urlFilename - } else { - name = blockCid.String() + ".bin" - } - setContentDispositionHeader(w, name, "attachment") - - // Set remaining headers - modtime := addCacheControlHeaders(w, r, contentPath, blockCid) - w.Header().Set("Content-Type", "application/vnd.ipld.raw") - w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^) - - // ServeContent will take care of - // If-None-Match+Etag, Content-Length and range requests - _, dataSent, _ := ServeContent(w, r, name, modtime, content) - - if dataSent { - // Update metrics - i.rawBlockGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) - } -} diff --git a/core/corehttp/gateway/handler_car.go b/core/corehttp/gateway/handler_car.go deleted file mode 100644 index f58bccfd7ae..00000000000 --- a/core/corehttp/gateway/handler_car.go +++ /dev/null @@ -1,98 +0,0 @@ -package gateway - -import ( - "context" - "fmt" - "net/http" - "time" - - cid "github.com/ipfs/go-cid" - blocks "github.com/ipfs/go-libipfs/blocks" - coreiface "github.com/ipfs/interface-go-ipfs-core" - ipath "github.com/ipfs/interface-go-ipfs-core/path" - gocar "github.com/ipld/go-car" - selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -// serveCAR returns a CAR stream for specific DAG+selector -func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, carVersion string, begin time.Time) { - ctx, span := spanTrace(ctx, "ServeCAR", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) - defer span.End() - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - switch carVersion { - case "": // noop, client does not care about version - case "1": // noop, we support this - default: - err := fmt.Errorf("only version=1 is supported") - webError(w, "unsupported CAR version", err, http.StatusBadRequest) - return - } - rootCid := resolvedPath.Cid() - - // Set Content-Disposition - var name string - if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" { - name = urlFilename - } else { - name = rootCid.String() + ".car" - } - setContentDispositionHeader(w, name, "attachment") - - // Set Cache-Control (same logic as for a regular files) - addCacheControlHeaders(w, r, contentPath, rootCid) - - // Weak Etag W/ because we can't guarantee byte-for-byte identical - // responses, but still want to benefit from HTTP Caching. Two CAR - // responses for the same CID and selector will be logically equivalent, - // but when CAR is streamed, then in theory, blocks may arrive from - // datastore in non-deterministic order. - etag := `W/` + getEtag(r, rootCid) - w.Header().Set("Etag", etag) - - // Finish early if Etag match - if r.Header.Get("If-None-Match") == etag { - w.WriteHeader(http.StatusNotModified) - return - } - - // Make it clear we don't support range-requests over a car stream - // Partial downloads and resumes should be handled using requests for - // sub-DAGs and IPLD selectors: https://github.com/ipfs/go-ipfs/issues/8769 - w.Header().Set("Accept-Ranges", "none") - - w.Header().Set("Content-Type", "application/vnd.ipld.car; version=1") - w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^) - - // Same go-car settings as dag.export command - store := dagStore{dag: i.api.Dag(), ctx: ctx} - - // TODO: support selectors passed as request param: https://github.com/ipfs/kubo/issues/8769 - dag := gocar.Dag{Root: rootCid, Selector: selectorparse.CommonSelector_ExploreAllRecursively} - car := gocar.NewSelectiveCar(ctx, store, []gocar.Dag{dag}, gocar.TraverseLinksOnlyOnce()) - - if err := car.Write(w); err != nil { - // We return error as a trailer, however it is not something browsers can access - // (https://github.com/mdn/browser-compat-data/issues/14703) - // Due to this, we suggest client always verify that - // the received CAR stream response is matching requested DAG selector - w.Header().Set("X-Stream-Error", err.Error()) - return - } - - // Update metrics - i.carStreamGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) -} - -// FIXME(@Jorropo): https://github.com/ipld/go-car/issues/315 -type dagStore struct { - dag coreiface.APIDagService - ctx context.Context -} - -func (ds dagStore) Get(_ context.Context, c cid.Cid) (blocks.Block, error) { - return ds.dag.Get(ds.ctx, c) -} diff --git a/core/corehttp/gateway/handler_codec.go b/core/corehttp/gateway/handler_codec.go deleted file mode 100644 index ac219f16545..00000000000 --- a/core/corehttp/gateway/handler_codec.go +++ /dev/null @@ -1,255 +0,0 @@ -package gateway - -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/core/corehttp/gateway/assets" - "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/multicodec" - mc "github.com/multiformats/go-multicodec" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -// codecToContentType maps the supported IPLD codecs to the HTTP Content -// Type they should have. -var codecToContentType = map[mc.Code]string{ - mc.Json: "application/json", - mc.Cbor: "application/cbor", - mc.DagJson: "application/vnd.ipld.dag-json", - mc.DagCbor: "application/vnd.ipld.dag-cbor", -} - -// contentTypeToRaw maps the HTTP Content Type to the respective codec that -// allows raw response without any conversion. -var contentTypeToRaw = map[string][]mc.Code{ - "application/json": {mc.Json, mc.DagJson}, - "application/cbor": {mc.Cbor, mc.DagCbor}, -} - -// contentTypeToCodec maps the HTTP Content Type to the respective codec. We -// only add here the codecs that we want to convert-to-from. -var contentTypeToCodec = map[string]mc.Code{ - "application/vnd.ipld.dag-json": mc.DagJson, - "application/vnd.ipld.dag-cbor": mc.DagCbor, -} - -// contentTypeToExtension maps the HTTP Content Type to the respective file -// extension, used in Content-Disposition header when downloading the file. -var contentTypeToExtension = map[string]string{ - "application/json": ".json", - "application/vnd.ipld.dag-json": ".json", - "application/cbor": ".cbor", - "application/vnd.ipld.dag-cbor": ".cbor", -} - -func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, requestedContentType string) { - ctx, span := spanTrace(ctx, "ServeCodec", trace.WithAttributes(attribute.String("path", resolvedPath.String()), attribute.String("requestedContentType", requestedContentType))) - defer span.End() - - cidCodec := mc.Code(resolvedPath.Cid().Prefix().Codec) - responseContentType := requestedContentType - - // If the resolved path still has some remainder, return error for now. - // TODO: handle this when we have IPLD Patch (https://ipld.io/specs/patch/) via HTTP PUT - // TODO: (depends on https://github.com/ipfs/kubo/issues/4801 and https://github.com/ipfs/kubo/issues/4782) - if resolvedPath.Remainder() != "" { - path := strings.TrimSuffix(resolvedPath.String(), resolvedPath.Remainder()) - err := fmt.Errorf("%q of %q could not be returned: reading IPLD Kinds other than Links (CBOR Tag 42) is not implemented: try reading %q instead", resolvedPath.Remainder(), resolvedPath.String(), path) - webError(w, "unsupported pathing", err, http.StatusNotImplemented) - return - } - - // If no explicit content type was requested, the response will have one based on the codec from the CID - if requestedContentType == "" { - cidContentType, ok := codecToContentType[cidCodec] - if !ok { - // Should not happen unless function is called with wrong parameters. - err := fmt.Errorf("content type not found for codec: %v", cidCodec) - webError(w, "internal error", err, http.StatusInternalServerError) - return - } - responseContentType = cidContentType - } - - // Set HTTP headers (for caching etc) - modtime := addCacheControlHeaders(w, r, contentPath, resolvedPath.Cid()) - name := setCodecContentDisposition(w, r, resolvedPath, responseContentType) - w.Header().Set("Content-Type", responseContentType) - w.Header().Set("X-Content-Type-Options", "nosniff") - - // No content type is specified by the user (via Accept, or format=). However, - // we support this format. Let's handle it. - if requestedContentType == "" { - isDAG := cidCodec == mc.DagJson || cidCodec == mc.DagCbor - acceptsHTML := strings.Contains(r.Header.Get("Accept"), "text/html") - download := r.URL.Query().Get("download") == "true" - - if isDAG && acceptsHTML && !download { - i.serveCodecHTML(ctx, w, r, resolvedPath, contentPath) - } else { - // This covers CIDs with codec 'json' and 'cbor' as those do not have - // an explicit requested content type. - i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime) - } - - return - } - - // If DAG-JSON or DAG-CBOR was requested using corresponding plain content type - // return raw block as-is, without conversion - skipCodecs, ok := contentTypeToRaw[requestedContentType] - if ok { - for _, skipCodec := range skipCodecs { - if skipCodec == cidCodec { - i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime) - return - } - } - } - - // Otherwise, the user has requested a specific content type (a DAG-* variant). - // Let's first get the codecs that can be used with this content type. - toCodec, ok := contentTypeToCodec[requestedContentType] - if !ok { - // This is never supposed to happen unless function is called with wrong parameters. - err := fmt.Errorf("unsupported content type: %s", requestedContentType) - webError(w, err.Error(), err, http.StatusInternalServerError) - return - } - - // This handles DAG-* conversions and validations. - i.serveCodecConverted(ctx, w, r, resolvedPath, contentPath, toCodec, modtime) -} - -func (i *handler) 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 := assets.DagTemplate.Execute(w, assets.DagTemplateData{ - Path: contentPath.String(), - CID: resolvedPath.Cid().String(), - CodecName: cidCodec.String(), - CodecHex: fmt.Sprintf("0x%x", uint64(cidCodec)), - }); err != nil { - webError(w, "failed to generate HTML listing for this DAG: try fetching raw block with ?format=raw", err, http.StatusInternalServerError) - } -} - -// serveCodecRaw returns the raw block without any conversion -func (i *handler) serveCodecRaw(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, name string, modtime time.Time) { - blockCid := resolvedPath.Cid() - blockReader, err := i.api.Block().Get(ctx, resolvedPath) - if err != nil { - webError(w, "ipfs block get "+blockCid.String(), err, http.StatusInternalServerError) - return - } - block, err := io.ReadAll(blockReader) - if err != nil { - webError(w, "ipfs block get "+blockCid.String(), err, http.StatusInternalServerError) - return - } - content := bytes.NewReader(block) - - // ServeContent will take care of - // If-None-Match+Etag, Content-Length and range requests - _, _, _ = ServeContent(w, r, name, modtime, content) -} - -// serveCodecConverted returns payload converted to codec specified in toCodec -func (i *handler) serveCodecConverted(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, toCodec mc.Code, modtime time.Time) { - obj, err := i.api.Dag().Get(ctx, resolvedPath.Cid()) - if err != nil { - webError(w, "ipfs dag get "+html.EscapeString(resolvedPath.String()), err, http.StatusInternalServerError) - return - } - - universal, ok := obj.(ipldlegacy.UniversalNode) - if !ok { - err = fmt.Errorf("%T is not a valid IPLD node", obj) - webError(w, err.Error(), err, http.StatusInternalServerError) - return - } - finalNode := universal.(ipld.Node) - - encoder, err := multicodec.LookupEncoder(uint64(toCodec)) - if err != nil { - webError(w, err.Error(), err, http.StatusInternalServerError) - return - } - - // Ensure IPLD node conforms to the codec specification. - var buf bytes.Buffer - err = encoder(finalNode, &buf) - if err != nil { - webError(w, err.Error(), err, http.StatusInternalServerError) - return - } - - // Sets correct Last-Modified header. This code is borrowed from the standard - // library (net/http/server.go) as we cannot use serveFile. - if !(modtime.IsZero() || modtime.Equal(unixEpochTime)) { - w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat)) - } - - _, _ = w.Write(buf.Bytes()) -} - -func setCodecContentDisposition(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentType string) string { - var dispType, name string - - ext, ok := contentTypeToExtension[contentType] - if !ok { - // Should never happen. - ext = ".bin" - } - - if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" { - name = urlFilename - } else { - name = resolvedPath.Cid().String() + ext - } - - // JSON should be inlined, but ?download=true should still override - if r.URL.Query().Get("download") == "true" { - dispType = "attachment" - } else { - switch ext { - case ".json": // codecs that serialize to JSON can be rendered by browsers - dispType = "inline" - default: // everything else is assumed binary / opaque bytes - dispType = "attachment" - } - } - - setContentDispositionHeader(w, name, dispType) - return name -} - -func getDagIndexEtag(dagCid cid.Cid) string { - return `"DagIndex-` + assets.AssetHash + `_CID-` + dagCid.String() + `"` -} diff --git a/core/corehttp/gateway/handler_ipns_record.go b/core/corehttp/gateway/handler_ipns_record.go deleted file mode 100644 index 47786c5b7fd..00000000000 --- a/core/corehttp/gateway/handler_ipns_record.go +++ /dev/null @@ -1,71 +0,0 @@ -package gateway - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - "time" - - "github.com/gogo/protobuf/proto" - ipns_pb "github.com/ipfs/go-ipns/pb" - path "github.com/ipfs/go-path" - ipath "github.com/ipfs/interface-go-ipfs-core/path" - "go.uber.org/zap" -) - -func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) { - if contentPath.Namespace() != "ipns" { - err := fmt.Errorf("%s is not an IPNS link", contentPath.String()) - webError(w, err.Error(), err, http.StatusBadRequest) - return - } - - key := contentPath.String() - key = strings.TrimSuffix(key, "/") - if strings.Count(key, "/") > 2 { - err := errors.New("cannot find ipns key for subpath") - webError(w, err.Error(), err, http.StatusBadRequest) - return - } - - rawRecord, err := i.api.Routing().Get(ctx, key) - if err != nil { - webError(w, err.Error(), err, http.StatusInternalServerError) - return - } - - var record ipns_pb.IpnsEntry - err = proto.Unmarshal(rawRecord, &record) - if err != nil { - webError(w, err.Error(), err, http.StatusInternalServerError) - return - } - - // Set cache control headers based on the TTL set in the IPNS record. If the - // TTL is not present, we use the Last-Modified tag. We are tracking IPNS - // caching on: https://github.com/ipfs/kubo/issues/1818. - // TODO: use addCacheControlHeaders once #1818 is fixed. - w.Header().Set("Etag", getEtag(r, resolvedPath.Cid())) - if record.Ttl != nil { - seconds := int(time.Duration(*record.Ttl).Seconds()) - w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", seconds)) - } else { - w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) - } - - // Set Content-Disposition - var name string - if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" { - name = urlFilename - } else { - name = path.SplitList(key)[2] + ".ipns-record" - } - setContentDispositionHeader(w, name, "attachment") - - w.Header().Set("Content-Type", "application/vnd.ipfs.ipns-record") - w.Header().Set("X-Content-Type-Options", "nosniff") - - _, _ = w.Write(rawRecord) -} diff --git a/core/corehttp/gateway/handler_tar.go b/core/corehttp/gateway/handler_tar.go deleted file mode 100644 index f5a7a67137f..00000000000 --- a/core/corehttp/gateway/handler_tar.go +++ /dev/null @@ -1,91 +0,0 @@ -package gateway - -import ( - "context" - "html" - "net/http" - "time" - - "github.com/ipfs/go-libipfs/files" - ipath "github.com/ipfs/interface-go-ipfs-core/path" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "go.uber.org/zap" -) - -var unixEpochTime = time.Unix(0, 0) - -func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) { - ctx, span := spanTrace(ctx, "ServeTAR", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) - defer span.End() - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - // Get Unixfs file - file, err := i.api.Unixfs().Get(ctx, resolvedPath) - if err != nil { - webError(w, "ipfs cat "+html.EscapeString(contentPath.String()), err, http.StatusBadRequest) - return - } - defer file.Close() - - rootCid := resolvedPath.Cid() - - // Set Cache-Control and read optional Last-Modified time - modtime := addCacheControlHeaders(w, r, contentPath, rootCid) - - // Weak Etag W/ because we can't guarantee byte-for-byte identical - // responses, but still want to benefit from HTTP Caching. Two TAR - // responses for the same CID will be logically equivalent, - // but when TAR is streamed, then in theory, files and directories - // may arrive in different order (depends on TAR lib and filesystem/inodes). - etag := `W/` + getEtag(r, rootCid) - w.Header().Set("Etag", etag) - - // Finish early if Etag match - if r.Header.Get("If-None-Match") == etag { - w.WriteHeader(http.StatusNotModified) - return - } - - // Set Content-Disposition - var name string - if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" { - name = urlFilename - } else { - name = rootCid.String() + ".tar" - } - setContentDispositionHeader(w, name, "attachment") - - // Construct the TAR writer - tarw, err := files.NewTarWriter(w) - if err != nil { - webError(w, "could not build tar writer", err, http.StatusInternalServerError) - return - } - defer tarw.Close() - - // Sets correct Last-Modified header. This code is borrowed from the standard - // library (net/http/server.go) as we cannot use serveFile without throwing the entire - // TAR into the memory first. - if !(modtime.IsZero() || modtime.Equal(unixEpochTime)) { - w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat)) - } - - w.Header().Set("Content-Type", "application/x-tar") - w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^) - - // The TAR has a top-level directory (or file) named by the CID. - if err := tarw.WriteFile(file, rootCid.String()); err != nil { - w.Header().Set("X-Stream-Error", err.Error()) - // Trailer headers do not work in web browsers - // (see https://github.com/mdn/browser-compat-data/issues/14703) - // and we have limited options around error handling in browser contexts. - // To improve UX/DX, we finish response stream with error message, allowing client to - // (1) detect error by having corrupted TAR - // (2) be able to reason what went wrong by instecting the tail of TAR stream - _, _ = w.Write([]byte(err.Error())) - return - } -} diff --git a/core/corehttp/gateway/handler_test.go b/core/corehttp/gateway/handler_test.go deleted file mode 100644 index d08dc295305..00000000000 --- a/core/corehttp/gateway/handler_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package gateway - -import "testing" - -func TestEtagMatch(t *testing.T) { - for _, test := range []struct { - header string // value in If-None-Match HTTP header - cidEtag string - dirEtag string - expected bool // expected result of etagMatch(header, cidEtag, dirEtag) - }{ - {"", `"etag"`, "", false}, // no If-None-Match - {"", "", `"etag"`, false}, // no If-None-Match - {`"etag"`, `"etag"`, "", true}, // file etag match - {`W/"etag"`, `"etag"`, "", true}, // file etag match - {`"foo", W/"bar", W/"etag"`, `"etag"`, "", true}, // file etag match (array) - {`"foo",W/"bar",W/"etag"`, `"etag"`, "", true}, // file etag match (compact array) - {`"etag"`, "", `W/"etag"`, true}, // dir etag match - {`"etag"`, "", `W/"etag"`, true}, // dir etag match - {`W/"etag"`, "", `W/"etag"`, true}, // dir etag match - {`*`, `"etag"`, "", true}, // wildcard etag match - } { - result := etagMatch(test.header, test.cidEtag, test.dirEtag) - if result != test.expected { - t.Fatalf("unexpected result of etagMatch(%q, %q, %q), got %t, expected %t", test.header, test.cidEtag, test.dirEtag, result, test.expected) - } - } -} diff --git a/core/corehttp/gateway/handler_unixfs.go b/core/corehttp/gateway/handler_unixfs.go deleted file mode 100644 index 9962d468c90..00000000000 --- a/core/corehttp/gateway/handler_unixfs.go +++ /dev/null @@ -1,45 +0,0 @@ -package gateway - -import ( - "context" - "fmt" - "html" - "net/http" - "time" - - "github.com/ipfs/go-libipfs/files" - ipath "github.com/ipfs/interface-go-ipfs-core/path" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "go.uber.org/zap" -) - -func (i *handler) serveUnixFS(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) { - ctx, span := spanTrace(ctx, "ServeUnixFS", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) - defer span.End() - - // Handling UnixFS - dr, err := i.api.Unixfs().Get(ctx, resolvedPath) - if err != nil { - webError(w, "ipfs cat "+html.EscapeString(contentPath.String()), err, http.StatusBadRequest) - return - } - defer dr.Close() - - // Handling Unixfs file - if f, ok := dr.(files.File); ok { - logger.Debugw("serving unixfs file", "path", contentPath) - i.serveFile(ctx, w, r, resolvedPath, contentPath, f, begin) - return - } - - // Handling Unixfs directory - dir, ok := dr.(files.Directory) - if !ok { - internalWebError(w, fmt.Errorf("unsupported UnixFS type")) - return - } - - logger.Debugw("serving unixfs directory", "path", contentPath) - i.serveDirectory(ctx, w, r, resolvedPath, contentPath, dir, begin, logger) -} diff --git a/core/corehttp/gateway/handler_unixfs__redirects.go b/core/corehttp/gateway/handler_unixfs__redirects.go deleted file mode 100644 index 98715cb2a56..00000000000 --- a/core/corehttp/gateway/handler_unixfs__redirects.go +++ /dev/null @@ -1,287 +0,0 @@ -package gateway - -import ( - "fmt" - "io" - "net/http" - gopath "path" - "strconv" - "strings" - - redirects "github.com/ipfs/go-ipfs-redirects-file" - "github.com/ipfs/go-libipfs/files" - ipath "github.com/ipfs/interface-go-ipfs-core/path" - "go.uber.org/zap" -) - -// Resolving a UnixFS path involves determining if the provided `path.Path` exists and returning the `path.Resolved` -// corresponding to that path. For UnixFS, path resolution is more involved. -// -// When a path under requested CID does not exist, Gateway will check if a `_redirects` file exists -// underneath the root CID of the path, and apply rules defined there. -// See sepcification introduced in: https://github.com/ipfs/specs/pull/290 -// -// Scenario 1: -// If a path exists, we always return the `path.Resolved` corresponding to that path, regardless of the existence of a `_redirects` file. -// -// Scenario 2: -// If a path does not exist, usually we should return a `nil` resolution path and an error indicating that the path -// doesn't exist. However, a `_redirects` file may exist and contain a redirect rule that redirects that path to a different path. -// We need to evaluate the rule and perform the redirect if present. -// -// Scenario 3: -// Another possibility is that the path corresponds to a rewrite rule (i.e. a rule with a status of 200). -// In this case, we don't perform a redirect, but do need to return a `path.Resolved` and `path.Path` corresponding to -// the rewrite destination path. -// -// Note that for security reasons, redirect rules are only processed when the request has origin isolation. -// See https://github.com/ipfs/specs/pull/290 for more information. -func (i *handler) serveRedirectsIfPresent(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, logger *zap.SugaredLogger) (newResolvedPath ipath.Resolved, newContentPath ipath.Path, continueProcessing bool, hadMatchingRule bool) { - redirectsFile := i.getRedirectsFile(r, contentPath, logger) - if redirectsFile != nil { - redirectRules, err := i.getRedirectRules(r, redirectsFile) - if err != nil { - internalWebError(w, err) - return nil, nil, false, true - } - - redirected, newPath, err := i.handleRedirectsFileRules(w, r, contentPath, redirectRules) - if err != nil { - err = fmt.Errorf("trouble processing _redirects file at %q: %w", redirectsFile.String(), err) - internalWebError(w, err) - return nil, nil, false, true - } - - if redirected { - return nil, nil, false, true - } - - // 200 is treated as a rewrite, so update the path and continue - if newPath != "" { - // Reassign contentPath and resolvedPath since the URL was rewritten - contentPath = ipath.New(newPath) - resolvedPath, err = i.api.ResolvePath(r.Context(), contentPath) - if err != nil { - internalWebError(w, err) - return nil, nil, false, true - } - - return resolvedPath, contentPath, true, true - } - } - // No matching rule, paths remain the same, continue regular processing - return resolvedPath, contentPath, true, false -} - -func (i *handler) handleRedirectsFileRules(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, redirectRules []redirects.Rule) (redirected bool, newContentPath string, err error) { - // Attempt to match a rule to the URL path, and perform the corresponding redirect or rewrite - pathParts := strings.Split(contentPath.String(), "/") - if len(pathParts) > 3 { - // All paths should start with /ipfs/cid/, so get the path after that - urlPath := "/" + strings.Join(pathParts[3:], "/") - rootPath := strings.Join(pathParts[:3], "/") - // Trim off the trailing / - urlPath = strings.TrimSuffix(urlPath, "/") - - for _, rule := range redirectRules { - // Error right away if the rule is invalid - if !rule.MatchAndExpandPlaceholders(urlPath) { - continue - } - - // We have a match! - - // Rewrite - if rule.Status == 200 { - // Prepend the rootPath - toPath := rootPath + rule.To - return false, toPath, nil - } - - // Or 4xx - if rule.Status == 404 || rule.Status == 410 || rule.Status == 451 { - toPath := rootPath + rule.To - content4xxPath := ipath.New(toPath) - err := i.serve4xx(w, r, content4xxPath, rule.Status) - return true, toPath, err - } - - // Or redirect - if rule.Status >= 301 && rule.Status <= 308 { - http.Redirect(w, r, rule.To, rule.Status) - return true, "", nil - } - } - } - - // No redirects matched - return false, "", nil -} - -func (i *handler) getRedirectRules(r *http.Request, redirectsFilePath ipath.Resolved) ([]redirects.Rule, error) { - // Convert the path into a file node - node, err := i.api.Unixfs().Get(r.Context(), redirectsFilePath) - if err != nil { - return nil, fmt.Errorf("could not get _redirects: %w", err) - } - defer node.Close() - - // Convert the node into a file - f, ok := node.(files.File) - if !ok { - return nil, fmt.Errorf("could not parse _redirects: %w", err) - } - - // Parse redirect rules from file - redirectRules, err := redirects.Parse(f) - if err != nil { - return nil, fmt.Errorf("could not parse _redirects: %w", err) - } - - return redirectRules, nil -} - -// Returns a resolved path to the _redirects file located in the root CID path of the requested path -func (i *handler) getRedirectsFile(r *http.Request, contentPath ipath.Path, logger *zap.SugaredLogger) ipath.Resolved { - // contentPath is the full ipfs path to the requested resource, - // regardless of whether path or subdomain resolution is used. - rootPath := getRootPath(contentPath) - - // Check for _redirects file. - // Any path resolution failures are ignored and we just assume there's no _redirects file. - // Note that ignoring these errors also ensures that the use of the empty CID (bafkqaaa) in tests doesn't fail. - path := ipath.Join(rootPath, "_redirects") - resolvedPath, err := i.api.ResolvePath(r.Context(), path) - if err != nil { - return nil - } - return resolvedPath -} - -// Returns the root CID Path for the given path -func getRootPath(path ipath.Path) ipath.Path { - parts := strings.Split(path.String(), "/") - return ipath.New(gopath.Join("/", path.Namespace(), parts[2])) -} - -func (i *handler) serve4xx(w http.ResponseWriter, r *http.Request, content4xxPath ipath.Path, status int) error { - resolved4xxPath, err := i.api.ResolvePath(r.Context(), content4xxPath) - if err != nil { - return err - } - - node, err := i.api.Unixfs().Get(r.Context(), resolved4xxPath) - if err != nil { - return err - } - defer node.Close() - - f, ok := node.(files.File) - if !ok { - return fmt.Errorf("could not convert node for %d page to file", status) - } - - size, err := f.Size() - if err != nil { - return fmt.Errorf("could not get size of %d page", status) - } - - log.Debugf("using _redirects: custom %d file at %q", status, content4xxPath) - w.Header().Set("Content-Type", "text/html") - w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) - addCacheControlHeaders(w, r, content4xxPath, resolved4xxPath.Cid()) - w.WriteHeader(status) - _, err = io.CopyN(w, f, size) - return err -} - -func hasOriginIsolation(r *http.Request) bool { - _, gw := r.Context().Value(GatewayHostnameKey).(string) - _, dnslink := r.Context().Value(DNSLinkHostnameKey).(string) - - if gw || dnslink { - return true - } - - return false -} - -func isUnixfsResponseFormat(responseFormat string) bool { - // The implicit response format is UnixFS - return responseFormat == "" -} - -// Deprecated: legacy ipfs-404.html files are superseded by _redirects file -// This is provided only for backward-compatibility, until websites migrate -// to 404s managed via _redirects file (https://github.com/ipfs/specs/pull/290) -func (i *handler) serveLegacy404IfPresent(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool { - resolved404Path, ctype, err := i.searchUpTreeFor404(r, contentPath) - if err != nil { - return false - } - - dr, err := i.api.Unixfs().Get(r.Context(), resolved404Path) - if err != nil { - return false - } - defer dr.Close() - - f, ok := dr.(files.File) - if !ok { - return false - } - - size, err := f.Size() - if err != nil { - return false - } - - log.Debugw("using pretty 404 file", "path", contentPath) - w.Header().Set("Content-Type", ctype) - w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) - w.WriteHeader(http.StatusNotFound) - _, err = io.CopyN(w, f, size) - return err == nil -} - -func (i *handler) searchUpTreeFor404(r *http.Request, contentPath ipath.Path) (ipath.Resolved, string, error) { - filename404, ctype, err := preferred404Filename(r.Header.Values("Accept")) - if err != nil { - return nil, "", err - } - - pathComponents := strings.Split(contentPath.String(), "/") - - for idx := len(pathComponents); idx >= 3; idx-- { - pretty404 := gopath.Join(append(pathComponents[0:idx], filename404)...) - parsed404Path := ipath.New("/" + pretty404) - if parsed404Path.IsValid() != nil { - break - } - resolvedPath, err := i.api.ResolvePath(r.Context(), parsed404Path) - if err != nil { - continue - } - return resolvedPath, ctype, nil - } - - return nil, "", fmt.Errorf("no pretty 404 in any parent folder") -} - -func preferred404Filename(acceptHeaders []string) (string, string, error) { - // If we ever want to offer a 404 file for a different content type - // then this function will need to parse q weightings, but for now - // the presence of anything matching HTML is enough. - for _, acceptHeader := range acceptHeaders { - accepted := strings.Split(acceptHeader, ",") - for _, spec := range accepted { - contentType := strings.SplitN(spec, ";", 1)[0] - switch contentType { - case "*/*", "text/*", "text/html": - return "ipfs-404.html", "text/html", nil - } - } - } - - return "", "", fmt.Errorf("there is no 404 file for the requested content types") -} diff --git a/core/corehttp/gateway/handler_unixfs_dir.go b/core/corehttp/gateway/handler_unixfs_dir.go deleted file mode 100644 index 8a66d4ea9ed..00000000000 --- a/core/corehttp/gateway/handler_unixfs_dir.go +++ /dev/null @@ -1,209 +0,0 @@ -package gateway - -import ( - "context" - "net/http" - "net/url" - gopath "path" - "strings" - "time" - - "github.com/dustin/go-humanize" - cid "github.com/ipfs/go-cid" - "github.com/ipfs/go-libipfs/files" - path "github.com/ipfs/go-path" - "github.com/ipfs/go-path/resolver" - options "github.com/ipfs/interface-go-ipfs-core/options" - ipath "github.com/ipfs/interface-go-ipfs-core/path" - "github.com/ipfs/kubo/core/corehttp/gateway/assets" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "go.uber.org/zap" -) - -// serveDirectory returns the best representation of UnixFS directory -// -// It will return index.html if present, or generate directory listing otherwise. -func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, dir files.Directory, begin time.Time, logger *zap.SugaredLogger) { - ctx, span := spanTrace(ctx, "ServeDirectory", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) - defer span.End() - - // HostnameOption might have constructed an IPNS/IPFS path using the Host header. - // In this case, we need the original path for constructing redirects - // and links that match the requested URL. - // For example, http://example.net would become /ipns/example.net, and - // the redirects and links would end up as http://example.net/ipns/example.net - requestURI, err := url.ParseRequestURI(r.RequestURI) - if err != nil { - webError(w, "failed to parse request path", err, http.StatusInternalServerError) - return - } - originalURLPath := requestURI.Path - - // Ensure directory paths end with '/' - if originalURLPath[len(originalURLPath)-1] != '/' { - // don't redirect to trailing slash if it's go get - // https://github.com/ipfs/kubo/pull/3963 - goget := r.URL.Query().Get("go-get") == "1" - if !goget { - 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 := originalURLPath + suffix - logger.Debugw("directory location moved permanently", "status", http.StatusMovedPermanently) - http.Redirect(w, r, redirectURL, http.StatusMovedPermanently) - return - } - } - - // Check if directory has index.html, if so, serveFile - idxPath := ipath.Join(contentPath, "index.html") - idx, err := i.api.Unixfs().Get(ctx, idxPath) - switch err.(type) { - case nil: - f, ok := idx.(files.File) - if !ok { - internalWebError(w, files.ErrNotReader) - return - } - - logger.Debugw("serving index.html file", "path", idxPath) - // write to request - i.serveFile(ctx, w, r, resolvedPath, idxPath, f, begin) - return - case resolver.ErrNoLink: - logger.Debugw("no index.html; noop", "path", idxPath) - default: - internalWebError(w, err) - return - } - - // See statusResponseWriter.WriteHeader - // and https://github.com/ipfs/kubo/issues/7164 - // Note: this needs to occur before listingTemplate.Execute otherwise we get - // superfluous response.WriteHeader call from prometheus/client_golang - if w.Header().Get("Location") != "" { - logger.Debugw("location moved permanently", "status", http.StatusMovedPermanently) - w.WriteHeader(http.StatusMovedPermanently) - return - } - - // 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") - - // Generated dir index requires custom Etag (output may change between go-ipfs versions) - dirEtag := getDirListingEtag(resolvedPath.Cid()) - w.Header().Set("Etag", dirEtag) - - if r.Method == http.MethodHead { - logger.Debug("return as request's HTTP method is HEAD") - return - } - - // Optimization: use Unixfs.Ls without resolving children, but using the - // cumulative DAG size as the file size. This allows for a fast listing - // while keeping a good enough Size field. - results, err := i.api.Unixfs().Ls(ctx, - resolvedPath, - options.Unixfs.ResolveChildren(false), - options.Unixfs.UseCumulativeSize(true), - ) - if err != nil { - internalWebError(w, err) - return - } - - dirListing := make([]assets.DirectoryItem, 0, len(results)) - for link := range results { - if link.Err != nil { - internalWebError(w, err) - return - } - - hash := link.Cid.String() - di := assets.DirectoryItem{ - Size: humanize.Bytes(uint64(link.Size)), - Name: link.Name, - Path: gopath.Join(originalURLPath, link.Name), - Hash: hash, - ShortHash: assets.ShortHash(hash), - } - dirListing = append(dirListing, di) - } - - // construct the correct back link - // https://github.com/ipfs/kubo/issues/1365 - backLink := originalURLPath - - // don't go further up than /ipfs/$hash/ - pathSplit := path.SplitList(contentPath.String()) - switch { - // skip backlink when listing a content root - case len(pathSplit) == 3: // url: /ipfs/$hash - backLink = "" - - // skip backlink when listing a content root - case len(pathSplit) == 4 && pathSplit[3] == "": // url: /ipfs/$hash/ - backLink = "" - - // add the correct link depending on whether the path ends with a slash - default: - if strings.HasSuffix(backLink, "/") { - backLink += ".." - } else { - backLink += "/.." - } - } - - size := "?" - if s, err := dir.Size(); err == nil { - // Size may not be defined/supported. Continue anyways. - size = humanize.Bytes(uint64(s)) - } - - hash := resolvedPath.Cid().String() - - // Gateway root URL to be used when linking to other rootIDs. - // This will be blank unless subdomain or DNSLink resolution is being used - // for this request. - var gwURL string - - // Get gateway hostname and build gateway URL. - if h, ok := r.Context().Value(GatewayHostnameKey).(string); ok { - gwURL = "//" + h - } else { - gwURL = "" - } - - dnslink := assets.HasDNSLinkOrigin(gwURL, contentPath.String()) - - // See comment above where originalUrlPath is declared. - tplData := assets.DirectoryTemplateData{ - GatewayURL: gwURL, - DNSLink: dnslink, - Listing: dirListing, - Size: size, - Path: contentPath.String(), - Breadcrumbs: assets.Breadcrumbs(contentPath.String(), dnslink), - BackLink: backLink, - Hash: hash, - } - - logger.Debugw("request processed", "tplDataDNSLink", dnslink, "tplDataSize", size, "tplDataBackLink", backLink, "tplDataHash", hash) - - if err := assets.DirectoryTemplate.Execute(w, tplData); err != nil { - internalWebError(w, err) - return - } - - // Update metrics - i.unixfsGenDirGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) -} - -func getDirListingEtag(dirCid cid.Cid) string { - return `"DirIndex-` + assets.AssetHash + `_CID-` + dirCid.String() + `"` -} diff --git a/core/corehttp/gateway/handler_unixfs_file.go b/core/corehttp/gateway/handler_unixfs_file.go deleted file mode 100644 index a4f7d4cd9e2..00000000000 --- a/core/corehttp/gateway/handler_unixfs_file.go +++ /dev/null @@ -1,103 +0,0 @@ -package gateway - -import ( - "context" - "fmt" - "io" - "mime" - "net/http" - gopath "path" - "strings" - "time" - - "github.com/gabriel-vasile/mimetype" - "github.com/ipfs/go-libipfs/files" - ipath "github.com/ipfs/interface-go-ipfs-core/path" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -// serveFile returns data behind a file along with HTTP headers based on -// the file itself, its CID and the contentPath used for accessing it. -func (i *handler) serveFile(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, file files.File, begin time.Time) { - _, span := spanTrace(ctx, "ServeFile", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) - defer span.End() - - // Set Cache-Control and read optional Last-Modified time - modtime := addCacheControlHeaders(w, r, contentPath, resolvedPath.Cid()) - - // Set Content-Disposition - name := addContentDispositionHeader(w, r, contentPath) - - // Prepare size value for Content-Length HTTP header (set inside of http.ServeContent) - size, err := file.Size() - if err != nil { - http.Error(w, "cannot serve files with unknown sizes", http.StatusBadGateway) - return - } - - if size == 0 { - // We override null files to 200 to avoid issues with fragment caching reverse proxies. - // Also whatever you are asking for, it's cheaper to just give you the complete file (nothing). - // TODO: remove this if clause once https://github.com/golang/go/issues/54794 is fixed in two latest releases of go - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusOK) - return - } - - // Lazy seeker enables efficient range-requests and HTTP HEAD responses - content := &lazySeeker{ - size: size, - reader: file, - } - - // Calculate deterministic value for Content-Type HTTP header - // (we prefer to do it here, rather than using implicit sniffing in http.ServeContent) - var ctype string - if _, isSymlink := file.(*files.Symlink); isSymlink { - // We should be smarter about resolving symlinks but this is the - // "most correct" we can be without doing that. - ctype = "inode/symlink" - } else { - ctype = mime.TypeByExtension(gopath.Ext(name)) - if ctype == "" { - // uses https://github.com/gabriel-vasile/mimetype library to determine the content type. - // Fixes https://github.com/ipfs/kubo/issues/7252 - mimeType, err := mimetype.DetectReader(content) - if err != nil { - http.Error(w, fmt.Sprintf("cannot detect content-type: %s", err.Error()), http.StatusInternalServerError) - return - } - - ctype = mimeType.String() - _, err = content.Seek(0, io.SeekStart) - if err != nil { - http.Error(w, "seeker can't seek", http.StatusInternalServerError) - return - } - } - // Strip the encoding from the HTML Content-Type header and let the - // browser figure it out. - // - // Fixes https://github.com/ipfs/kubo/issues/2203 - if strings.HasPrefix(ctype, "text/html;") { - ctype = "text/html" - } - } - // Setting explicit Content-Type to avoid mime-type sniffing on the client - // (unifies behavior across gateways and web browsers) - w.Header().Set("Content-Type", ctype) - - // special fixup around redirects - w = &statusResponseWriter{w} - - // ServeContent will take care of - // If-None-Match+Etag, Content-Length and range requests - _, dataSent, _ := ServeContent(w, r, name, modtime, content) - - // Was response successful? - if dataSent { - // Update metrics - i.unixfsFileGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) - } -} diff --git a/core/corehttp/gateway/lazyseek.go b/core/corehttp/gateway/lazyseek.go deleted file mode 100644 index 0f4920fad43..00000000000 --- a/core/corehttp/gateway/lazyseek.go +++ /dev/null @@ -1,60 +0,0 @@ -package gateway - -import ( - "fmt" - "io" -) - -// The HTTP server uses seek to determine the file size. Actually _seeking_ can -// be slow so we wrap the seeker in a _lazy_ seeker. -type lazySeeker struct { - reader io.ReadSeeker - - size int64 - offset int64 - realOffset int64 -} - -func (s *lazySeeker) Seek(offset int64, whence int) (int64, error) { - switch whence { - case io.SeekEnd: - return s.Seek(s.size+offset, io.SeekStart) - case io.SeekCurrent: - return s.Seek(s.offset+offset, io.SeekStart) - case io.SeekStart: - if offset < 0 { - return s.offset, fmt.Errorf("invalid seek offset") - } - s.offset = offset - return s.offset, nil - default: - return s.offset, fmt.Errorf("invalid whence: %d", whence) - } -} - -func (s *lazySeeker) Read(b []byte) (int, error) { - // If we're past the end, EOF. - if s.offset >= s.size { - return 0, io.EOF - } - - // actually seek - for s.offset != s.realOffset { - off, err := s.reader.Seek(s.offset, io.SeekStart) - if err != nil { - return 0, err - } - s.realOffset = off - } - off, err := s.reader.Read(b) - s.realOffset += int64(off) - s.offset += int64(off) - return off, err -} - -func (s *lazySeeker) Close() error { - if closer, ok := s.reader.(io.Closer); ok { - return closer.Close() - } - return nil -} diff --git a/core/corehttp/gateway/lazyseek_test.go b/core/corehttp/gateway/lazyseek_test.go deleted file mode 100644 index 09997a79796..00000000000 --- a/core/corehttp/gateway/lazyseek_test.go +++ /dev/null @@ -1,136 +0,0 @@ -package gateway - -import ( - "fmt" - "io" - "strings" - "testing" -) - -type badSeeker struct { - io.ReadSeeker -} - -var errBadSeek = fmt.Errorf("bad seeker") - -func (bs badSeeker) Seek(offset int64, whence int) (int64, error) { - off, err := bs.ReadSeeker.Seek(0, io.SeekCurrent) - if err != nil { - panic(err) - } - return off, errBadSeek -} - -func TestLazySeekerError(t *testing.T) { - underlyingBuffer := strings.NewReader("fubar") - s := &lazySeeker{ - reader: badSeeker{underlyingBuffer}, - size: underlyingBuffer.Size(), - } - off, err := s.Seek(0, io.SeekEnd) - if err != nil { - t.Fatal(err) - } - if off != s.size { - t.Fatal("expected to seek to the end") - } - - // shouldn't have actually seeked. - b, err := io.ReadAll(s) - if err != nil { - t.Fatal(err) - } - if len(b) != 0 { - t.Fatal("expected to read nothing") - } - - // shouldn't need to actually seek. - off, err = s.Seek(0, io.SeekStart) - if err != nil { - t.Fatal(err) - } - if off != 0 { - t.Fatal("expected to seek to the start") - } - b, err = io.ReadAll(s) - if err != nil { - t.Fatal(err) - } - if string(b) != "fubar" { - t.Fatal("expected to read string") - } - - // should fail the second time. - off, err = s.Seek(0, io.SeekStart) - if err != nil { - t.Fatal(err) - } - if off != 0 { - t.Fatal("expected to seek to the start") - } - // right here... - b, err = io.ReadAll(s) - if err == nil { - t.Fatalf("expected an error, got output %s", string(b)) - } - if err != errBadSeek { - t.Fatalf("expected a bad seek error, got %s", err) - } - if len(b) != 0 { - t.Fatalf("expected to read nothing") - } -} - -func TestLazySeeker(t *testing.T) { - underlyingBuffer := strings.NewReader("fubar") - s := &lazySeeker{ - reader: underlyingBuffer, - size: underlyingBuffer.Size(), - } - expectByte := func(b byte) { - t.Helper() - var buf [1]byte - n, err := io.ReadFull(s, buf[:]) - if err != nil { - t.Fatal(err) - } - if n != 1 { - t.Fatalf("expected to read one byte, read %d", n) - } - if buf[0] != b { - t.Fatalf("expected %b, got %b", b, buf[0]) - } - } - expectSeek := func(whence int, off, expOff int64, expErr string) { - t.Helper() - n, err := s.Seek(off, whence) - if expErr == "" { - if err != nil { - t.Fatal("unexpected seek error: ", err) - } - } else { - if err == nil || err.Error() != expErr { - t.Fatalf("expected %s, got %s", err, expErr) - } - } - if n != expOff { - t.Fatalf("expected offset %d, got, %d", expOff, n) - } - } - - expectSeek(io.SeekEnd, 0, s.size, "") - b, err := io.ReadAll(s) - if err != nil { - t.Fatal(err) - } - if len(b) != 0 { - t.Fatal("expected to read nothing") - } - expectSeek(io.SeekEnd, -1, s.size-1, "") - expectByte('r') - expectSeek(io.SeekStart, 0, 0, "") - expectByte('f') - expectSeek(io.SeekCurrent, 1, 2, "") - expectByte('b') - expectSeek(io.SeekCurrent, -100, 3, "invalid seek offset") -} diff --git a/core/corehttp/hostname.go b/core/corehttp/hostname.go index cb6d7fbc5c3..adc47ab4ddb 100644 --- a/core/corehttp/hostname.go +++ b/core/corehttp/hostname.go @@ -10,10 +10,10 @@ import ( "strings" cid "github.com/ipfs/go-cid" + "github.com/ipfs/go-libipfs/gateway" namesys "github.com/ipfs/go-namesys" core "github.com/ipfs/kubo/core" coreapi "github.com/ipfs/kubo/core/coreapi" - "github.com/ipfs/kubo/core/corehttp/gateway" "github.com/libp2p/go-libp2p/core/peer" dns "github.com/miekg/dns" diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index 3294cd137ab..a74bfbf3e03 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -7,7 +7,7 @@ go 1.18 replace github.com/ipfs/kubo => ./../../.. require ( - github.com/ipfs/go-libipfs v0.4.0 + github.com/ipfs/go-libipfs v0.4.1-0.20230130233950-a005a5006496 github.com/ipfs/interface-go-ipfs-core v0.10.0 github.com/ipfs/kubo v0.0.0-00010101000000-000000000000 github.com/libp2p/go-libp2p v0.24.2 diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index ece45c198d4..eb37654f786 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -548,8 +548,8 @@ github.com/ipfs/go-ipld-legacy v0.1.1 h1:BvD8PEuqwBHLTKqlGFTHSwrwFOMkVESEvwIYwR2 github.com/ipfs/go-ipld-legacy v0.1.1/go.mod h1:8AyKFCjgRPsQFf15ZQgDB8Din4DML/fOmKZkkFkrIEg= github.com/ipfs/go-ipns v0.3.0 h1:ai791nTgVo+zTuq2bLvEGmWP1M0A6kGTXUsgv/Yq67A= github.com/ipfs/go-ipns v0.3.0/go.mod h1:3cLT2rbvgPZGkHJoPO1YMJeh6LtkxopCkKFcio/wE24= -github.com/ipfs/go-libipfs v0.4.0 h1:TkUxJGjtPnSzAgkw7VjS0/DBay3MPjmTBa4dGdUQCDE= -github.com/ipfs/go-libipfs v0.4.0/go.mod h1:XsU2cP9jBhDrXoJDe0WxikB8XcVmD3k2MEZvB3dbYu8= +github.com/ipfs/go-libipfs v0.4.1-0.20230130233950-a005a5006496 h1:RVI31GQCFODREpasIFyVFkS6PjJT2bMwr/Bgr9Ryql4= +github.com/ipfs/go-libipfs v0.4.1-0.20230130233950-a005a5006496/go.mod h1:AAPvZADZ80i+QhGCWNWCsx8IGY0t9C+IBEngLeYtySY= github.com/ipfs/go-log v0.0.1/go.mod h1:kL1d2/hzSpI0thNYjiKfjanbVNU+IIGA/WnNESY9leM= github.com/ipfs/go-log v1.0.2/go.mod h1:1MNjMxe0u6xvJZgeqbJ8vdo2TKaGwZ1a0Bpza+sr2Sk= github.com/ipfs/go-log v1.0.3/go.mod h1:OsLySYkwIbiSUR/yBTdv1qPtcE4FW3WPWk/ewz9Ru+A= @@ -597,7 +597,7 @@ github.com/ipfs/interface-go-ipfs-core v0.10.0 h1:b/psL1oqJcySdQAsIBfW5ZJJkOAsYl github.com/ipfs/interface-go-ipfs-core v0.10.0/go.mod h1:F3EcmDy53GFkF0H3iEJpfJC320fZ/4G60eftnItrrJ0= github.com/ipld/edelweiss v0.2.0 h1:KfAZBP8eeJtrLxLhi7r3N0cBCo7JmwSRhOJp3WSpNjk= github.com/ipld/edelweiss v0.2.0/go.mod h1:FJAzJRCep4iI8FOFlRriN9n0b7OuX3T/S9++NpBDmA4= -github.com/ipld/go-car v0.4.0 h1:U6W7F1aKF/OJMHovnOVdst2cpQE5GhmHibQkAixgNcQ= +github.com/ipld/go-car v0.5.0 h1:kcCEa3CvYMs0iE5BzD5sV7O2EwMiCIp3uF8tA6APQT8= github.com/ipld/go-car/v2 v2.5.1 h1:U2ux9JS23upEgrJScW8VQuxmE94560kYxj9CQUpcfmk= github.com/ipld/go-codec-dagpb v1.3.0/go.mod h1:ga4JTU3abYApDC3pZ00BC2RSvC3qfBb9MSJkMLSwnhA= github.com/ipld/go-codec-dagpb v1.5.0 h1:RspDRdsJpLfgCI0ONhTAnbHdySGD4t+LHSPK4X1+R0k= diff --git a/go.mod b/go.mod index 3d91f40e77f..5edf9f68a46 100644 --- a/go.mod +++ b/go.mod @@ -7,14 +7,12 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/cenkalti/backoff/v4 v4.1.3 github.com/ceramicnetwork/go-dag-jose v0.1.0 - github.com/cespare/xxhash v1.1.0 github.com/cheggaaa/pb v1.0.29 github.com/coreos/go-systemd/v22 v22.5.0 github.com/dustin/go-humanize v1.0.0 github.com/elgris/jsondiff v0.0.0-20160530203242-765b5c24c302 github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5 github.com/fsnotify/fsnotify v1.6.0 - github.com/gabriel-vasile/mimetype v1.4.1 github.com/gogo/protobuf v1.3.2 github.com/google/uuid v1.3.0 github.com/hashicorp/go-multierror v1.1.1 @@ -41,14 +39,13 @@ require ( github.com/ipfs/go-ipfs-pinner v0.2.1 github.com/ipfs/go-ipfs-posinfo v0.0.1 github.com/ipfs/go-ipfs-provider v0.8.1 - github.com/ipfs/go-ipfs-redirects-file v0.1.1 github.com/ipfs/go-ipfs-routing v0.3.0 github.com/ipfs/go-ipfs-util v0.0.2 github.com/ipfs/go-ipld-format v0.4.0 github.com/ipfs/go-ipld-git v0.1.1 github.com/ipfs/go-ipld-legacy v0.1.1 github.com/ipfs/go-ipns v0.3.0 - github.com/ipfs/go-libipfs v0.4.0 + github.com/ipfs/go-libipfs v0.4.1-0.20230130233950-a005a5006496 github.com/ipfs/go-log v1.0.5 github.com/ipfs/go-log/v2 v2.5.1 github.com/ipfs/go-merkledag v0.9.0 @@ -62,7 +59,7 @@ require ( github.com/ipfs/go-unixfsnode v1.5.1 github.com/ipfs/go-verifcid v0.0.2 github.com/ipfs/interface-go-ipfs-core v0.10.0 - github.com/ipld/go-car v0.4.0 + github.com/ipld/go-car v0.5.0 github.com/ipld/go-car/v2 v2.5.1 github.com/ipld/go-codec-dagpb v1.5.0 github.com/ipld/go-ipld-prime v0.19.0 @@ -121,6 +118,7 @@ require ( github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect + github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/containerd/cgroups v1.0.4 // indirect github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 // indirect @@ -136,6 +134,7 @@ require ( github.com/felixge/httpsnoop v1.0.2 // indirect github.com/flynn/noise v1.0.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect + github.com/gabriel-vasile/mimetype v1.4.1 // indirect github.com/go-kit/log v0.2.0 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logr/logr v1.2.3 // indirect @@ -161,6 +160,7 @@ require ( github.com/ipfs/go-ipfs-delay v0.0.1 // indirect github.com/ipfs/go-ipfs-ds-help v1.1.0 // indirect github.com/ipfs/go-ipfs-pq v0.0.2 // indirect + github.com/ipfs/go-ipfs-redirects-file v0.1.1 // indirect github.com/ipfs/go-ipld-cbor v0.0.6 // indirect github.com/ipfs/go-peertaskqueue v0.8.0 // indirect github.com/ipld/edelweiss v0.2.0 // indirect diff --git a/go.sum b/go.sum index 471acc8080b..fefe9bf7160 100644 --- a/go.sum +++ b/go.sum @@ -570,8 +570,8 @@ github.com/ipfs/go-ipld-legacy v0.1.1 h1:BvD8PEuqwBHLTKqlGFTHSwrwFOMkVESEvwIYwR2 github.com/ipfs/go-ipld-legacy v0.1.1/go.mod h1:8AyKFCjgRPsQFf15ZQgDB8Din4DML/fOmKZkkFkrIEg= github.com/ipfs/go-ipns v0.3.0 h1:ai791nTgVo+zTuq2bLvEGmWP1M0A6kGTXUsgv/Yq67A= github.com/ipfs/go-ipns v0.3.0/go.mod h1:3cLT2rbvgPZGkHJoPO1YMJeh6LtkxopCkKFcio/wE24= -github.com/ipfs/go-libipfs v0.4.0 h1:TkUxJGjtPnSzAgkw7VjS0/DBay3MPjmTBa4dGdUQCDE= -github.com/ipfs/go-libipfs v0.4.0/go.mod h1:XsU2cP9jBhDrXoJDe0WxikB8XcVmD3k2MEZvB3dbYu8= +github.com/ipfs/go-libipfs v0.4.1-0.20230130233950-a005a5006496 h1:RVI31GQCFODREpasIFyVFkS6PjJT2bMwr/Bgr9Ryql4= +github.com/ipfs/go-libipfs v0.4.1-0.20230130233950-a005a5006496/go.mod h1:AAPvZADZ80i+QhGCWNWCsx8IGY0t9C+IBEngLeYtySY= github.com/ipfs/go-log v0.0.1/go.mod h1:kL1d2/hzSpI0thNYjiKfjanbVNU+IIGA/WnNESY9leM= github.com/ipfs/go-log v1.0.2/go.mod h1:1MNjMxe0u6xvJZgeqbJ8vdo2TKaGwZ1a0Bpza+sr2Sk= github.com/ipfs/go-log v1.0.3/go.mod h1:OsLySYkwIbiSUR/yBTdv1qPtcE4FW3WPWk/ewz9Ru+A= @@ -623,8 +623,8 @@ github.com/ipfs/interface-go-ipfs-core v0.10.0 h1:b/psL1oqJcySdQAsIBfW5ZJJkOAsYl github.com/ipfs/interface-go-ipfs-core v0.10.0/go.mod h1:F3EcmDy53GFkF0H3iEJpfJC320fZ/4G60eftnItrrJ0= github.com/ipld/edelweiss v0.2.0 h1:KfAZBP8eeJtrLxLhi7r3N0cBCo7JmwSRhOJp3WSpNjk= github.com/ipld/edelweiss v0.2.0/go.mod h1:FJAzJRCep4iI8FOFlRriN9n0b7OuX3T/S9++NpBDmA4= -github.com/ipld/go-car v0.4.0 h1:U6W7F1aKF/OJMHovnOVdst2cpQE5GhmHibQkAixgNcQ= -github.com/ipld/go-car v0.4.0/go.mod h1:Uslcn4O9cBKK9wqHm/cLTFacg6RAPv6LZx2mxd2Ypl4= +github.com/ipld/go-car v0.5.0 h1:kcCEa3CvYMs0iE5BzD5sV7O2EwMiCIp3uF8tA6APQT8= +github.com/ipld/go-car v0.5.0/go.mod h1:ppiN5GWpjOZU9PgpAZ9HbZd9ZgSpwPMr48fGRJOWmvE= github.com/ipld/go-car/v2 v2.5.1 h1:U2ux9JS23upEgrJScW8VQuxmE94560kYxj9CQUpcfmk= github.com/ipld/go-car/v2 v2.5.1/go.mod h1:jKjGOqoCj5zn6KjnabD6JbnCsMntqU2hLiU6baZVO3E= github.com/ipld/go-codec-dagpb v1.3.0/go.mod h1:ga4JTU3abYApDC3pZ00BC2RSvC3qfBb9MSJkMLSwnhA=