feat: remove writable gateway (#9743)

Co-authored-by: Marcin Rataj <lidel@lidel.org>
This commit is contained in:
Henrique Dias 2023-03-27 15:19:55 +02:00 committed by GitHub
parent bf7d0fc99d
commit 88d431c812
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 18 additions and 464 deletions

View File

@ -163,7 +163,7 @@ Headers.
cmds.StringOption(initProfileOptionKwd, "Configuration profiles to apply for --init. See ipfs init --help for more"),
cmds.StringOption(routingOptionKwd, "Overrides the routing option").WithDefault(routingOptionDefaultKwd),
cmds.BoolOption(mountKwd, "Mounts IPFS to the filesystem using FUSE (experimental)"),
cmds.BoolOption(writableKwd, "Enable legacy Gateway.Writable (deprecated)"),
cmds.BoolOption(writableKwd, "Enable legacy Gateway.Writable (REMOVED)"),
cmds.StringOption(ipfsMountKwd, "Path to the mountpoint for IPFS (if using --mount). Defaults to config setting."),
cmds.StringOption(ipnsMountKwd, "Path to the mountpoint for IPNS (if using --mount). Defaults to config setting."),
cmds.BoolOption(unrestrictedAPIAccessKwd, "Allow API access to unlisted hashes"),
@ -679,7 +679,7 @@ func serveHTTPApi(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, error
for _, listener := range listeners {
// we might have listened to /tcp/0 - let's see what we are listing on
fmt.Printf("API server listening on %s\n", listener.Multiaddr())
fmt.Printf("RPC API server listening on %s\n", listener.Multiaddr())
// Browsers require TCP.
switch listener.Addr().Network() {
case "tcp", "tcp4", "tcp6":
@ -692,9 +692,9 @@ func serveHTTPApi(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, error
// only the webui objects are allowed.
// if you know what you're doing, go ahead and pass --unrestricted-api.
unrestricted, _ := req.Options[unrestrictedAPIAccessKwd].(bool)
gatewayOpt := corehttp.GatewayOption(false, corehttp.WebUIPaths...)
gatewayOpt := corehttp.GatewayOption(corehttp.WebUIPaths...)
if unrestricted {
gatewayOpt = corehttp.GatewayOption(true, "/ipfs", "/ipns")
gatewayOpt = corehttp.GatewayOption("/ipfs", "/ipns")
}
var opts = []corehttp.ServeOption{
@ -804,7 +804,7 @@ func serveHTTPGateway(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, e
}
if writable {
log.Error("serveHTTPGateway: legacy Gateway.Writable is DEPRECATED and will be removed or changed in future versions. If you are still using this, provide feedback in https://github.com/ipfs/specs/issues/375")
log.Fatalf("Support for Gateway.Writable and --writable has been REMOVED. Please remove it from your config file or CLI. Modern replacement tracked in https://github.com/ipfs/specs/issues/375")
}
listeners, err := sockets.TakeListeners("io.ipfs.gateway")
@ -837,13 +837,8 @@ func serveHTTPGateway(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, e
}
// we might have listened to /tcp/0 - let's see what we are listing on
gwType := "readonly"
if writable {
gwType = "writable"
}
for _, listener := range listeners {
fmt.Printf("Gateway (%s) server listening on %s\n", gwType, listener.Multiaddr())
fmt.Printf("Gateway server listening on %s\n", listener.Multiaddr())
}
cmdctx := *cctx
@ -852,7 +847,7 @@ func serveHTTPGateway(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, e
var opts = []corehttp.ServeOption{
corehttp.MetricsCollectionOption("gateway"),
corehttp.HostnameOption(),
corehttp.GatewayOption(writable, "/ipfs", "/ipns"),
corehttp.GatewayOption("/ipfs", "/ipns"),
corehttp.VersionOption(),
corehttp.CheckVersionOption(),
corehttp.CommandsROOption(cmdctx),

View File

@ -94,7 +94,7 @@ func run(ipfsPath, watchPath string) error {
if *http {
addr := "/ip4/127.0.0.1/tcp/5001"
var opts = []corehttp.ServeOption{
corehttp.GatewayOption(true, "/ipfs", "/ipns"),
corehttp.GatewayOption("/ipfs", "/ipns"),
corehttp.WebUIOption,
corehttp.CommandsOption(cmdCtx(node, ipfsPath)),
}

View File

@ -38,8 +38,7 @@ type Gateway struct {
// should be redirected.
RootRedirect string
// DEPRECATED: Enables legacy PUT/POST request handling.
// Modern replacement tracked in https://github.com/ipfs/specs/issues/375
// REMOVED: modern replacement tracked in https://github.com/ipfs/specs/issues/375
Writable Flag `json:",omitempty"`
// PathPrefixes was removed: https://github.com/ipfs/go-ipfs/issues/7702

View File

@ -75,7 +75,7 @@ func ListenAndServe(n *core.IpfsNode, listeningMultiAddr string, options ...Serv
// we might have listened to /tcp/0 - let's see what we are listing on
addr = list.Multiaddr()
fmt.Printf("API server listening on %s\n", addr)
fmt.Printf("RPC API server listening on %s\n", addr)
return Serve(n, manet.NetListener(list), options...)
}

View File

@ -24,18 +24,13 @@ import (
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func GatewayOption(writable bool, paths ...string) ServeOption {
func GatewayOption(paths ...string) ServeOption {
return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
cfg, err := n.Repo.Config()
if err != nil {
return nil, err
}
api, err := coreapi.NewCoreAPI(n, options.Api.FetchBlocks(!cfg.Gateway.NoFetch))
if err != nil {
return nil, err
}
headers := make(map[string][]string, len(cfg.Gateway.HTTPHeaders))
for h, v := range cfg.Gateway.HTTPHeaders {
headers[http.CanonicalHeaderKey(h)] = v
@ -58,28 +53,6 @@ func GatewayOption(writable bool, paths ...string) ServeOption {
// By default, our HTTP handler is the gateway handler.
handler := gw.ServeHTTP
// If we have the writable gateway enabled, we have to replace our
// http handler by a handler that takes care of the different methods.
if writable {
writableGw := &writableGatewayHandler{
config: &gwConfig,
api: api,
}
handler = func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
writableGw.postHandler(w, r)
case http.MethodDelete:
writableGw.deleteHandler(w, r)
case http.MethodPut:
writableGw.putHandler(w, r)
default:
gw.ServeHTTP(w, r)
}
}
}
for _, p := range paths {
mux.HandleFunc(p+"/", handler)
}

View File

@ -124,7 +124,7 @@ func newTestServerAndNode(t *testing.T, ns mockNamesys) (*httptest.Server, iface
dh.Handler, err = makeHandler(n,
ts.Listener,
HostnameOption(),
GatewayOption(false, "/ipfs", "/ipns"),
GatewayOption("/ipfs", "/ipns"),
VersionOption(),
)
if err != nil {

View File

@ -1,265 +0,0 @@
package corehttp
import (
"context"
"fmt"
"net/http"
"os"
gopath "path"
cid "github.com/ipfs/go-cid"
ipld "github.com/ipfs/go-ipld-format"
"github.com/ipfs/go-libipfs/files"
"github.com/ipfs/go-libipfs/gateway"
dag "github.com/ipfs/go-merkledag"
"github.com/ipfs/go-mfs"
path "github.com/ipfs/go-path"
"github.com/ipfs/go-path/resolver"
iface "github.com/ipfs/interface-go-ipfs-core"
routing "github.com/libp2p/go-libp2p/core/routing"
)
const (
ipfsPathPrefix = "/ipfs/"
)
type writableGatewayHandler struct {
api iface.CoreAPI
config *gateway.Config
}
func (i *writableGatewayHandler) addUserHeaders(w http.ResponseWriter) {
for k, v := range i.config.Headers {
w.Header()[http.CanonicalHeaderKey(k)] = v
}
}
func (i *writableGatewayHandler) 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 *writableGatewayHandler) 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 *writableGatewayHandler) 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 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 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)
}

View File

@ -681,14 +681,9 @@ Type: `string` (url)
### `Gateway.Writable`
**DEPRECATED**: Enables legacy PUT/POST request handling.
**REMOVED**: this option no longer available as of [Kubo 0.20](https://github.com/ipfs/kubo/blob/master/docs/changelogs/v0.20.md).
This API is not standardized, and should not be used for new projects.
We are working on a modern replacement. IPIP can be tracked in [ipfs/specs#375](https://github.com/ipfs/specs/issues/375).
Default: `false`
Type: `bool`
We are working on developing a modern replacement. To support our efforts, please leave a comment describing your use case in [ipfs/specs#375](https://github.com/ipfs/specs/issues/375).
### `Gateway.PathPrefixes`

View File

@ -436,7 +436,7 @@ func TestGateway(t *testing.T) {
t.Run("verify gateway file", func(t *testing.T) {
t.Parallel()
r := regexp.MustCompile(`Gateway \(readonly\) server listening on (?P<addr>.+)\s`)
r := regexp.MustCompile(`Gateway server listening on (?P<addr>.+)\s`)
matches := r.FindStringSubmatch(node.Daemon.Stdout.String())
ma, err := multiaddr.NewMultiaddr(matches[1])
require.NoError(t, err)

View File

@ -214,13 +214,6 @@ test_init_ipfs() {
}
test_config_ipfs_gateway_writable() {
test_expect_success "prepare config -- gateway writable" '
test_config_set --bool Gateway.Writable true ||
test_fsh cat "\"$IPFS_PATH/config\""
'
}
test_wait_for_file() {
loops=$1
delay=$2
@ -247,7 +240,7 @@ test_set_address_vars() {
API_ADDR=$(convert_tcp_maddr $API_MADDR) &&
API_PORT=$(port_from_maddr $API_MADDR) &&
GWAY_MADDR=$(sed -n "s/^Gateway (.*) server listening on //p" "$daemon_output") &&
GWAY_MADDR=$(sed -n "s/^Gateway server listening on //p" "$daemon_output") &&
GWAY_ADDR=$(convert_tcp_maddr $GWAY_MADDR) &&
GWAY_PORT=$(port_from_maddr $GWAY_MADDR)
'

View File

@ -89,9 +89,9 @@ test_expect_success "ipfs daemon output looks good" '
echo "" >>expected_daemon &&
sed "s/^/Swarm listening on /" listen_addrs >>expected_daemon &&
sed "s/^/Swarm announcing /" local_addrs >>expected_daemon &&
echo "API server listening on '$API_MADDR'" >>expected_daemon &&
echo "RPC API server listening on '$API_MADDR'" >>expected_daemon &&
echo "WebUI: http://'$API_ADDR'/webui" >>expected_daemon &&
echo "Gateway (readonly) server listening on '$GWAY_MADDR'" >>expected_daemon &&
echo "Gateway server listening on '$GWAY_MADDR'" >>expected_daemon &&
echo "Daemon is ready" >>expected_daemon &&
test_cmp expected_daemon actual_daemon
'

View File

@ -1,136 +0,0 @@
#!/usr/bin/env bash
#
# Copyright (c) 2014 Christian Couder
# MIT Licensed; see the LICENSE file in this repository.
#
test_description="Test HTTP Gateway (Writable)"
. lib/test-lib.sh
test_init_ipfs
test_launch_ipfs_daemon --writable
test_expect_success "ipfs daemon --writable overrides config" '
curl -v -X POST http://$GWAY_ADDR/ipfs/ 2> outfile &&
grep "HTTP/1.1 201 Created" outfile &&
grep "Location: /ipfs/QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH" outfile
'
test_kill_ipfs_daemon
test_config_ipfs_gateway_writable
test_launch_ipfs_daemon --writable=false
test_expect_success "ipfs daemon --writable=false overrides Writable=true config" '
curl -v -X POST http://$GWAY_ADDR/ipfs/ 2> outfile &&
grep "HTTP/1.1 405 Method Not Allowed" outfile
'
test_kill_ipfs_daemon
test_launch_ipfs_daemon
port=$GWAY_PORT
test_expect_success "ipfs daemon up" '
pollEndpoint -host $GWAY_MADDR -v -tout=1s -tries=60 2>poll_apierr > poll_apiout ||
test_fsh cat poll_apierr || test_fsh cat poll_apiout
'
test_expect_success "deprecation notice is printed when Gateway.Writable=true" '
test_should_contain "legacy Gateway.Writable is DEPRECATED and will be removed or changed in future versions. If you are still using this, provide feedback in https://github.com/ipfs/specs/issues/375" daemon_err
'
test_expect_success "HTTP gateway gives access to sample file" '
curl -s -o welcome "http://$GWAY_ADDR/ipfs/$HASH_WELCOME_DOCS/readme" &&
grep "Hello and Welcome to IPFS!" welcome
'
test_expect_success "HTTP POST file gives Hash" '
echo "$RANDOM" >infile &&
URL="http://127.0.0.1:$port/ipfs/" &&
curl -svX POST --data-binary @infile "$URL" 2>curl_post.out &&
grep "HTTP/1.1 201 Created" curl_post.out &&
LOCATION=$(grep Location curl_post.out) &&
HASH=$(echo $LOCATION | cut -d":" -f2- |tr -d " \n\r")
'
test_expect_success "We can HTTP GET file just created" '
URL="http://127.0.0.1:${port}${HASH}" &&
curl -so outfile "$URL" &&
test_cmp infile outfile
'
test_expect_success "We got the correct hash" '
ADD_HASH="/ipfs/$(ipfs add -q infile)" &&
test "x$ADD_HASH" = "x$HASH" || test_fsh echo "$ADD_HASH != $HASH"
'
test_expect_success "HTTP GET empty directory" '
URL="http://127.0.0.1:$port/ipfs/$HASH_EMPTY_DIR/" &&
echo "GET $URL" &&
curl -so outfile "$URL" 2>curl_getEmpty.out &&
cat outfile | tr -s "\n" " " | grep "Index of /ipfs/<a href=\"/ipfs/$HASH_EMPTY_DIR\">$HASH_EMPTY_DIR</a>"
'
test_expect_success "HTTP PUT file to construct a hierarchy" '
echo "$RANDOM" >infile &&
URL="http://127.0.0.1:$port/ipfs/$HASH_EMPTY_DIR/test.txt" &&
echo "PUT $URL" &&
curl -svX PUT --data-binary @infile "$URL" 2>curl_put.out &&
grep "HTTP/1.1 201 Created" curl_put.out &&
LOCATION=$(grep Location curl_put.out) &&
HASH=$(expr "$LOCATION" : "< Location: /ipfs/\(.*\)/test.txt")
'
test_expect_success "We can HTTP GET file just created" '
URL="http://127.0.0.1:$port/ipfs/$HASH/test.txt" &&
echo "GET $URL" &&
curl -so outfile "$URL" &&
test_cmp infile outfile
'
test_expect_success "HTTP PUT file to append to existing hierarchy" '
echo "$RANDOM" >infile2 &&
URL="http://127.0.0.1:$port/ipfs/$HASH/test/test.txt" &&
echo "PUT $URL" &&
curl -svX PUT --data-binary @infile2 "$URL" 2>curl_putAgain.out &&
grep "HTTP/1.1 201 Created" curl_putAgain.out &&
LOCATION=$(grep Location curl_putAgain.out) &&
HASH=$(expr "$LOCATION" : "< Location: /ipfs/\(.*\)/test/test.txt")
'
test_expect_success "We can HTTP GET file just updated" '
URL="http://127.0.0.1:$port/ipfs/$HASH/test/test.txt" &&
echo "GET $URL" &&
curl -svo outfile2 "$URL" 2>curl_getAgain.out &&
test_cmp infile2 outfile2
'
test_expect_success "HTTP PUT to replace a directory" '
echo "$RANDOM" >infile3 &&
URL="http://127.0.0.1:$port/ipfs/$HASH/test" &&
echo "PUT $URL" &&
curl -svX PUT --data-binary @infile3 "$URL" 2>curl_putOverDirectory.out &&
grep "HTTP/1.1 201 Created" curl_putOverDirectory.out &&
LOCATION=$(grep Location curl_putOverDirectory.out) &&
HASH=$(expr "$LOCATION" : "< Location: /ipfs/\(.*\)/test")
'
test_expect_success "We can HTTP GET file just put over a directory" '
URL="http://127.0.0.1:$port/ipfs/$HASH/test" &&
echo "GET $URL" &&
curl -svo outfile3 "$URL" 2>curl_getOverDirectory.out &&
test_cmp infile3 outfile3
'
test_expect_success "HTTP PUT to /ipns fails" '
PEERID=`ipfs id --format="<id>"` &&
URL="http://127.0.0.1:$port/ipns/$PEERID/test.txt" &&
echo "PUT $URL" &&
curl -svX PUT --data-binary @infile1 "$URL" 2>curl_putIpns.out &&
grep "HTTP/1.1 400 Bad Request" curl_putIpns.out
'
test_kill_ipfs_daemon
test_done