@cryptotaxi247 / kubo / commits / 521a29956

fix and improve the writable gateway

1. Fix handling of PUT. The simple implementation was the correct implementation, I have no idea what was going on here. 2. Use MFS everywhere to reduce code duplication and add support for sharded directories. 3. _Correctly_ block IPNS. 4. Remove the dependency on `core.IpfsNode`. 5. Remove support for putting empty directories with a well-known CID. It was useless as directories are automatically created.

Steven Allen committed Jul 25, 2019 at 17:30 UTC 521a29956b35ceeba775d6f699edc02c29c1857e
5 files changed +155 -167
core/corehttp/gateway.go
+1 -1
@@ -87,7 +87,7 @@ func GatewayOption(writable bool, paths ...string) ServeOption {
87 "X-Stream-Output",
88 }, headers[ACEHeadersName]...))
89
90 - gateway := newGatewayHandler(n, GatewayConfig{
90 + gateway := newGatewayHandler(GatewayConfig{
91 Headers: headers,
92 Writable: writable,
93 PathPrefixes: cfg.Gateway.PathPrefixes,
core/corehttp/gateway_handler.go
+129 -152
@@ -2,30 +2,23 @@ package corehttp
2
3 import (
4 "context"
5 - "errors"
5 "fmt"
6 "io"
7 "net/http"
8 "net/url"
9 + "os"
10 gopath "path"
11 "runtime/debug"
12 "strings"
13 "time"
14
15 - "github.com/ipfs/go-ipfs/core"
16 - "github.com/ipfs/go-ipfs/dagutils"
17 - "github.com/ipfs/go-ipfs/namesys/resolve"
18 -
15 "github.com/dustin/go-humanize"
16 "github.com/ipfs/go-cid"
21 - chunker "github.com/ipfs/go-ipfs-chunker"
17 files "github.com/ipfs/go-ipfs-files"
23 - ipld "github.com/ipfs/go-ipld-format"
18 dag "github.com/ipfs/go-merkledag"
19 + "github.com/ipfs/go-mfs"
20 "github.com/ipfs/go-path"
21 "github.com/ipfs/go-path/resolver"
27 - ft "github.com/ipfs/go-unixfs"
28 - "github.com/ipfs/go-unixfs/importer"
22 coreiface "github.com/ipfs/interface-go-ipfs-core"
23 ipath "github.com/ipfs/interface-go-ipfs-core/path"
24 routing "github.com/libp2p/go-libp2p-core/routing"
@@ -40,27 +33,36 @@ const (
33 // gatewayHandler is a HTTP handler that serves IPFS objects (accessible by default at /ipfs/<path>)
34 // (it serves requests like GET /ipfs/QmVRzPKPzNtSrEzBFm2UZfxmPAgnaLke4DMcerbsGGSaFe/link)
35 type gatewayHandler struct {
43 - node *core.IpfsNode
36 config GatewayConfig
37 api coreiface.CoreAPI
38 }
39
48 -func newGatewayHandler(n *core.IpfsNode, c GatewayConfig, api coreiface.CoreAPI) *gatewayHandler {
40 +func newGatewayHandler(c GatewayConfig, api coreiface.CoreAPI) *gatewayHandler {
41 i := &gatewayHandler{
50 - node: n,
42 config: c,
43 api: api,
44 }
45 return i
46 }
47
57 -// TODO(cryptix): find these helpers somewhere else
58 -func (i *gatewayHandler) newDagFromReader(r io.Reader) (ipld.Node, error) {
59 - // TODO(cryptix): change and remove this helper once PR1136 is merged
60 - // return ufs.AddFromReader(i.node, r.Body)
61 - return importer.BuildDagFromReader(
62 - i.node.DAG,
63 - chunker.DefaultSplitter(r))
48 +func parseIpfsPath(p string) (cid.Cid, string, error) {
49 + rootPath, err := path.ParsePath(p)
50 + if err != nil {
51 + return cid.Cid{}, "", err
52 + }
53 +
54 + // Check the path.
55 + rsegs := rootPath.Segments()
56 + if rsegs[0] != "ipfs" {
57 + return cid.Cid{}, "", fmt.Errorf("WritableGateway: only ipfs paths supported")
58 + }
59 +
60 + rootCid, err := cid.Decode(rsegs[1])
61 + if err != nil {
62 + return cid.Cid{}, "", err
63 + }
64 +
65 + return rootCid, path.Join(rsegs[2:]), nil
66 }
67
68 func (i *gatewayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -160,10 +162,12 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
162
163 // Resolve path to the final DAG node for the ETag
164 resolvedPath, err := i.api.ResolvePath(r.Context(), parsedPath)
163 - if err == coreiface.ErrOffline && !i.node.IsOnline {
165 + switch err {
166 + case nil:
167 + case coreiface.ErrOffline:
168 webError(w, "ipfs resolve -r "+escapedURLPath, err, http.StatusServiceUnavailable)
169 return
166 - } else if err != nil {
170 + default:
171 webError(w, "ipfs resolve -r "+escapedURLPath, err, http.StatusNotFound)
172 return
173 }
@@ -395,102 +399,91 @@ func (i *gatewayHandler) postHandler(w http.ResponseWriter, r *http.Request) {
399 }
400
401 func (i *gatewayHandler) putHandler(w http.ResponseWriter, r *http.Request) {
398 - rootPath, err := path.ParsePath(r.URL.Path)
402 + ctx := r.Context()
403 + ds := i.api.Dag()
404 +
405 + // Parse the path
406 + rootCid, newPath, err := parseIpfsPath(r.URL.Path)
407 if err != nil {
400 - webError(w, "putHandler: IPFS path not valid", err, http.StatusBadRequest)
408 + webError(w, "WritableGateway: failed to parse the path", err, http.StatusBadRequest)
409 return
410 }
403 -
404 - rsegs := rootPath.Segments()
405 - if rsegs[0] == ipnsPathPrefix {
406 - webError(w, "putHandler: updating named entries not supported", errors.New("WritableGateway: ipns put not supported"), http.StatusBadRequest)
411 + if newPath == "" || newPath == "/" {
412 + http.Error(w, "WritableGateway: empty path", http.StatusBadRequest)
413 return
414 }
415 + newDirectory, newFileName := gopath.Split(newPath)
416
410 - var newnode ipld.Node
411 - if rsegs[len(rsegs)-1] == "QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn" {
412 - newnode = ft.EmptyDirNode()
413 - } else {
414 - putNode, err := i.newDagFromReader(r.Body)
415 - if err != nil {
416 - webError(w, "putHandler: Could not create DAG from request", err, http.StatusInternalServerError)
417 - return
418 - }
419 - newnode = putNode
420 - }
417 + // Resolve the old root.
418
422 - var newPath string
423 - if len(rsegs) > 1 {
424 - newPath = path.Join(rsegs[2:])
419 + rnode, err := ds.Get(ctx, rootCid)
420 + if err != nil {
421 + webError(w, "WritableGateway: Could not create DAG from request", err, http.StatusInternalServerError)
422 + return
423 }
424
427 - var newcid cid.Cid
428 - rnode, err := resolve.Resolve(r.Context(), i.node.Namesys, i.node.Resolver, rootPath)
429 - switch ev := err.(type) {
430 - case resolver.ErrNoLink:
431 - // ev.Node < node where resolve failed
432 - // ev.Name < new link
433 - // but we need to patch from the root
434 - c, err := cid.Decode(rsegs[1])
435 - if err != nil {
436 - webError(w, "putHandler: bad input path", err, http.StatusBadRequest)
437 - return
438 - }
439 -
440 - rnode, err := i.node.DAG.Get(r.Context(), c)
441 - if err != nil {
442 - webError(w, "putHandler: Could not create DAG from request", err, http.StatusInternalServerError)
443 - return
444 - }
445 -
446 - pbnd, ok := rnode.(*dag.ProtoNode)
447 - if !ok {
448 - webError(w, "Cannot read non protobuf nodes through gateway", dag.ErrNotProtobuf, http.StatusBadRequest)
449 - return
450 - }
451 -
452 - e := dagutils.NewDagEditor(pbnd, i.node.DAG)
453 - err = e.InsertNodeAtPath(r.Context(), newPath, newnode, ft.EmptyDirNode)
454 - if err != nil {
455 - webError(w, "putHandler: InsertNodeAtPath failed", err, http.StatusInternalServerError)
456 - return
457 - }
458 -
459 - nnode, err := e.Finalize(r.Context(), i.node.DAG)
460 - if err != nil {
461 - webError(w, "putHandler: could not get node", err, http.StatusInternalServerError)
462 - return
463 - }
425 + pbnd, ok := rnode.(*dag.ProtoNode)
426 + if !ok {
427 + webError(w, "Cannot read non protobuf nodes through gateway", dag.ErrNotProtobuf, http.StatusBadRequest)
428 + return
429 + }
430
465 - newcid = nnode.Cid()
431 + // Create the new file.
432 + newFilePath, err := i.api.Unixfs().Add(ctx, files.NewReaderFile(r.Body))
433 + if err != nil {
434 + webError(w, "WritableGateway: could not create DAG from request", err, http.StatusInternalServerError)
435 + return
436 + }
437
467 - case nil:
468 - pbnd, ok := rnode.(*dag.ProtoNode)
469 - if !ok {
470 - webError(w, "Cannot read non protobuf nodes through gateway", dag.ErrNotProtobuf, http.StatusBadRequest)
471 - return
472 - }
438 + newFile, err := ds.Get(ctx, newFilePath.Cid())
439 + if err != nil {
440 + webError(w, "WritableGateway: failed to resolve new file", err, http.StatusInternalServerError)
441 + return
442 + }
443
474 - pbnewnode, ok := newnode.(*dag.ProtoNode)
475 - if !ok {
476 - webError(w, "Cannot read non protobuf nodes through gateway", dag.ErrNotProtobuf, http.StatusBadRequest)
477 - return
478 - }
444 + // Patch the new file into the old root.
445
480 - // object set-data case
481 - pbnd.SetData(pbnewnode.Data())
446 + root, err := mfs.NewRoot(ctx, ds, pbnd, nil)
447 + if err != nil {
448 + webError(w, "WritableGateway: failed to create MFS root", err, http.StatusBadRequest)
449 + return
450 + }
451
483 - newcid = pbnd.Cid()
484 - err = i.node.DAG.Add(r.Context(), pbnd)
452 + if newDirectory != "" {
453 + err := mfs.Mkdir(root, newDirectory, mfs.MkdirOpts{Mkparents: true, Flush: false})
454 if err != nil {
486 - nnk := newnode.Cid()
487 - webError(w, fmt.Sprintf("putHandler: Could not add newnode(%q) to root(%q)", nnk.String(), newcid.String()), err, http.StatusInternalServerError)
455 + webError(w, "WritableGateway: failed to create MFS directory", err, http.StatusInternalServerError)
456 return
457 }
458 + }
459 + dirNode, err := mfs.Lookup(root, newDirectory)
460 + if err != nil {
461 + webError(w, "WritableGateway: failed to lookup directory", err, http.StatusInternalServerError)
462 + return
463 + }
464 + dir, ok := dirNode.(*mfs.Directory)
465 + if !ok {
466 + http.Error(w, "WritableGateway: target directory is not a directory", http.StatusBadRequest)
467 + return
468 + }
469 + err = dir.Unlink(newFileName)
470 + switch err {
471 + case os.ErrNotExist, nil:
472 default:
491 - webError(w, "could not resolve root DAG", ev, http.StatusInternalServerError)
473 + webError(w, "WritableGateway: failed to replace existing file", err, http.StatusBadRequest)
474 return
475 }
476 + err = dir.AddChild(newFileName, newFile)
477 + if err != nil {
478 + webError(w, "WritableGateway: failed to link file into directory", err, http.StatusInternalServerError)
479 + return
480 + }
481 + nnode, err := root.GetDirectory().GetNode()
482 + if err != nil {
483 + webError(w, "WritableGateway: failed to finalize", err, http.StatusInternalServerError)
484 + return
485 + }
486 + newcid := nnode.Cid()
487
488 i.addUserHeaders(w) // ok, _now_ write user's headers.
489 w.Header().Set("IPFS-Hash", newcid.String())
@@ -498,91 +491,75 @@ func (i *gatewayHandler) putHandler(w http.ResponseWriter, r *http.Request) {
491 }
492
493 func (i *gatewayHandler) deleteHandler(w http.ResponseWriter, r *http.Request) {
501 - urlPath := r.URL.Path
494 + ctx := r.Context()
495 +
496 + // parse the path
497
503 - p, err := path.ParsePath(urlPath)
498 + rootCid, newPath, err := parseIpfsPath(r.URL.Path)
499 if err != nil {
505 - webError(w, "failed to parse path", err, http.StatusBadRequest)
500 + webError(w, "WritableGateway: failed to parse the path", err, http.StatusBadRequest)
501 return
502 }
508 -
509 - c, components, err := path.SplitAbsPath(p)
510 - if err != nil {
511 - webError(w, "Could not split path", err, http.StatusInternalServerError)
503 + if newPath == "" || newPath == "/" {
504 + http.Error(w, "WritableGateway: empty path", http.StatusBadRequest)
505 return
506 }
507 + directory, filename := gopath.Split(newPath)
508 +
509 + // lookup the root
510
515 - pathNodes, err := i.resolvePathComponents(r.Context(), c, components)
511 + rootNodeIPLD, err := i.api.Dag().Get(ctx, rootCid)
512 if err != nil {
517 - webError(w, "Could not resolve path components", err, http.StatusBadRequest)
513 + webError(w, "WritableGateway: failed to resolve root CID", err, http.StatusInternalServerError)
514 return
515 }
520 -
521 - pbnd, ok := pathNodes[len(pathNodes)-1].(*dag.ProtoNode)
516 + rootNode, ok := rootNodeIPLD.(*dag.ProtoNode)
517 if !ok {
523 - webError(w, "Cannot read non protobuf nodes through gateway", dag.ErrNotProtobuf, http.StatusBadRequest)
518 + http.Error(w, "WritableGateway: empty path", http.StatusInternalServerError)
519 return
520 }
521
527 - // TODO(cyrptix): assumes len(pathNodes) > 1 - not found is an error above?
528 - err = pbnd.RemoveNodeLink(components[len(components)-1])
522 + // construct the mfs root
523 +
524 + root, err := mfs.NewRoot(ctx, i.api.Dag(), rootNode, nil)
525 if err != nil {
530 - webError(w, "Could not delete link", err, http.StatusBadRequest)
526 + webError(w, "WritableGateway: failed to construct the MFS root", err, http.StatusBadRequest)
527 return
528 }
529
534 - var newnode *dag.ProtoNode = pbnd
535 - for j := len(pathNodes) - 2; j >= 0; j-- {
536 - if err := i.node.DAG.Add(r.Context(), newnode); err != nil {
537 - webError(w, "Could not add node", err, http.StatusInternalServerError)
538 - return
539 - }
530 + // lookup the parent directory
531
541 - pathpb, ok := pathNodes[j].(*dag.ProtoNode)
542 - if !ok {
543 - webError(w, "Cannot read non protobuf nodes through gateway", dag.ErrNotProtobuf, http.StatusBadRequest)
544 - return
545 - }
546 -
547 - newnode, err = pathpb.UpdateNodeLink(components[j], newnode)
548 - if err != nil {
549 - webError(w, "Could not update node links", err, http.StatusInternalServerError)
550 - return
551 - }
532 + parentNode, err := mfs.Lookup(root, directory)
533 + if err != nil {
534 + webError(w, "WritableGateway: failed to look up parent", err, http.StatusInternalServerError)
535 + return
536 }
537
554 - if err := i.node.DAG.Add(r.Context(), newnode); err != nil {
555 - webError(w, "Could not add root node", err, http.StatusInternalServerError)
538 + parent, ok := parentNode.(*mfs.Directory)
539 + if !ok {
540 + http.Error(w, "WritableGateway: parent is not a directory", http.StatusInternalServerError)
541 return
542 }
543
559 - // Redirect to new path
560 - ncid := newnode.Cid()
544 + // delete the file
545
562 - i.addUserHeaders(w) // ok, _now_ write user's headers.
563 - w.Header().Set("IPFS-Hash", ncid.String())
564 - http.Redirect(w, r, gopath.Join(ipfsPathPrefix+ncid.String(), path.Join(components[:len(components)-1])), http.StatusCreated)
565 -}
566 -
567 -func (i *gatewayHandler) resolvePathComponents(
568 - ctx context.Context,
569 - c cid.Cid,
570 - components []string,
571 -) ([]ipld.Node, error) {
572 - tctx, cancel := context.WithTimeout(ctx, time.Minute)
573 - defer cancel()
574 -
575 - rootnd, err := i.node.Resolver.DAG.Get(tctx, c)
576 - if err != nil {
577 - return nil, fmt.Errorf("Could not resolve root object: %s", err)
546 + switch parent.Unlink(filename) {
547 + case nil, os.ErrNotExist:
548 + default:
549 + webError(w, "WritableGateway: failed to remove file", err, http.StatusInternalServerError)
550 + return
551 }
552
580 - pathNodes, err := i.node.Resolver.ResolveLinks(tctx, rootnd, components[:len(components)-1])
553 + nnode, err := root.GetDirectory().GetNode()
554 if err != nil {
582 - return nil, fmt.Errorf("Could not resolve parent object: %s", err)
555 + webError(w, "WritableGateway: failed to finalize", err, http.StatusInternalServerError)
556 }
557 + ncid := nnode.Cid()
558
585 - return pathNodes, nil
559 + i.addUserHeaders(w) // ok, _now_ write user's headers.
560 + w.Header().Set("IPFS-Hash", ncid.String())
561 + // note: StatusCreated is technically correct here as we created a new resource.
562 + http.Redirect(w, r, gopath.Join(ipfsPathPrefix+ncid.String(), directory), http.StatusCreated)
563 }
564
565 func (i *gatewayHandler) addUserHeaders(w http.ResponseWriter) {
go.mod
+2 -2
@@ -45,12 +45,12 @@ require (
45 github.com/ipfs/go-ipld-git v0.0.2
46 github.com/ipfs/go-ipns v0.0.1
47 github.com/ipfs/go-log v0.0.1
48 - github.com/ipfs/go-merkledag v0.2.0
48 + github.com/ipfs/go-merkledag v0.2.3
49 github.com/ipfs/go-metrics-interface v0.0.1
50 github.com/ipfs/go-metrics-prometheus v0.0.2
51 github.com/ipfs/go-mfs v0.1.0
52 github.com/ipfs/go-path v0.0.7
53 - github.com/ipfs/go-unixfs v0.2.0
53 + github.com/ipfs/go-unixfs v0.2.1
54 github.com/ipfs/go-verifcid v0.0.1
55 github.com/ipfs/hang-fds v0.0.1
56 github.com/ipfs/interface-go-ipfs-core v0.1.0
go.sum
+4 -2
@@ -301,6 +301,8 @@ github.com/ipfs/go-merkledag v0.1.0 h1:CAEXjRFEDPvealQj3TgEjV1IJckwjvmxAqtq5QSXJ
301 github.com/ipfs/go-merkledag v0.1.0/go.mod h1:SQiXrtSts3KGNmgOzMICy5c0POOpUNQLvB3ClKnBAlk=
302 github.com/ipfs/go-merkledag v0.2.0 h1:EAjIQCgZ6/DnOAlKY3+59j72FD9BsYtNaCRSmN0xIbU=
303 github.com/ipfs/go-merkledag v0.2.0/go.mod h1:SQiXrtSts3KGNmgOzMICy5c0POOpUNQLvB3ClKnBAlk=
304 +github.com/ipfs/go-merkledag v0.2.3 h1:aMdkK9G1hEeNvn3VXfiEMLY0iJnbiQQUHnM0HFJREsE=
305 +github.com/ipfs/go-merkledag v0.2.3/go.mod h1:SQiXrtSts3KGNmgOzMICy5c0POOpUNQLvB3ClKnBAlk=
306 github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg=
307 github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY=
308 github.com/ipfs/go-metrics-prometheus v0.0.2 h1:9i2iljLg12S78OhC6UAiXi176xvQGiZaGVF1CUVdE+s=
@@ -320,8 +322,8 @@ github.com/ipfs/go-todocounter v0.0.1/go.mod h1:l5aErvQc8qKE2r7NDMjmq5UNAvuZy0rC
322 github.com/ipfs/go-unixfs v0.0.4/go.mod h1:eIo/p9ADu/MFOuyxzwU+Th8D6xoxU//r590vUpWyfz8=
323 github.com/ipfs/go-unixfs v0.0.8 h1:AHahQ+gdNZd9BhKVLf8XP1EWeKa78eTzYgCygp7N/Pg=
324 github.com/ipfs/go-unixfs v0.0.8/go.mod h1:cK2vDJ7L4YnWB6oLefpVNesgx0x/zPTRVDw6B4Y+03U=
323 -github.com/ipfs/go-unixfs v0.2.0 h1:mfdI8rgsEifWfhLECrH2WphHvslNoPbdvlmsJ05Fu0M=
324 -github.com/ipfs/go-unixfs v0.2.0/go.mod h1:sy/j20FKUxdUYOl6GZb3wsQ593C2xk5r1i8U7W+5iio=
325 +github.com/ipfs/go-unixfs v0.2.1 h1:g51t9ODICFZ3F51FPivm8dE7NzYcdAQNUL9wGP5AYa0=
326 +github.com/ipfs/go-unixfs v0.2.1/go.mod h1:IwAAgul1UQIcNZzKPYZWOCijryFBeCV79cNubPzol+k=
327 github.com/ipfs/go-verifcid v0.0.1 h1:m2HI7zIuR5TFyQ1b79Da5N9dnnCP1vcu2QqawmWlK2E=
328 github.com/ipfs/go-verifcid v0.0.1/go.mod h1:5Hrva5KBeIog4A+UpqlaIU+DEstipcJYQQZc0g37pY0=
329 github.com/ipfs/hang-fds v0.0.1 h1:KGAxiGtJPT3THVRNT6yxgpdFPeX4ZemUjENOt6NlOn4=
test/sharness/t0111-gateway-writeable.sh
+19 -10
@@ -54,17 +54,8 @@ test_expect_success "We can HTTP GET file just created" '
54 test_cmp infile outfile
55 '
56
57 -test_expect_success "HTTP PUT empty directory" '
58 - URL="http://localhost:$port/ipfs/$HASH_EMPTY_DIR/" &&
59 - echo "PUT $URL" &&
60 - curl -svX PUT "$URL" 2>curl_putEmpty.out &&
61 - cat curl_putEmpty.out &&
62 - grep "Ipfs-Hash: $HASH_EMPTY_DIR" curl_putEmpty.out &&
63 - grep "Location: /ipfs/$HASH_EMPTY_DIR" curl_putEmpty.out &&
64 - grep "HTTP/1.1 201 Created" curl_putEmpty.out
65 -'
66 -
57 test_expect_success "HTTP GET empty directory" '
58 + URL="http://localhost:$port/ipfs/$HASH_EMPTY_DIR/" &&
59 echo "GET $URL" &&
60 curl -so outfile "$URL" 2>curl_getEmpty.out &&
61 grep "Index of /ipfs/$HASH_EMPTY_DIR/" outfile
@@ -105,6 +96,24 @@ test_expect_success "We can HTTP GET file just updated" '
96 test_cmp infile2 outfile2
97 '
98
99 +test_expect_success "HTTP PUT to replace a directory" '
100 + echo "$RANDOM" >infile3 &&
101 + URL="http://localhost:$port/ipfs/$HASH/test" &&
102 + echo "PUT $URL" &&
103 + curl -svX PUT --data-binary @infile3 "$URL" 2>curl_putOverDirectory.out &&
104 + grep "HTTP/1.1 201 Created" curl_putOverDirectory.out &&
105 + LOCATION=$(grep Location curl_putOverDirectory.out) &&
106 + HASH=$(expr "$LOCATION" : "< Location: /ipfs/\(.*\)/test")
107 +'
108 +
109 +test_expect_success "We can HTTP GET file just put over a directory" '
110 + URL="http://localhost:$port/ipfs/$HASH/test" &&
111 + echo "GET $URL" &&
112 + curl -svo outfile3 "$URL" 2>curl_getOverDirectory.out &&
113 + test_cmp infile3 outfile3
114 +'
115 +
116 +
117 test_kill_ipfs_daemon
118
119 test_done