ipfs-cluster/api/rest/restapi.go
Kishan Mohanbhai Sagathiya 5fb2b6ae95 Add PinPath/UnpinPath support.
Squashed commit of the following:

commit 38cf569c6aed77c46ee4e0f8baa4d1a9daf8f03e
Merge: d125f69 aaada42
Author: Hector Sanjuan <hsanjuan@users.noreply.github.com>
Date:   Wed Feb 20 11:02:00 2019 +0000

    Merge pull request #634 from ipfs/issue_450

    Support PinPath, UnpinPath (resolve before pinning)

commit aaada42054e1f1c7b2abb1270859d0de41a0e5d8
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Tue Feb 19 22:16:25 2019 +0530

    formatResponse accepts api.Pin and not api.PinSerial

commit b5da4bea045865814cc422da71827b44ddd44b90
Merge: ba59036 cc8dd7e
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Tue Feb 19 21:36:46 2019 +0530

    Merge branch 'master' into issue_450

commit ba5903649c1df1dba20f4d6f7e3573d6fe24921f
Merge: f002914 d59880c
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Mon Feb 18 08:41:11 2019 +0530

    Merge branch 'issue_450' of github.com:ipfs/ipfs-cluster into issue_450

commit f00291494c0c02621c2296cbb7ac71e4c23aa9ec
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Mon Feb 18 08:31:39 2019 +0530

    PinPath: more improvements

    Added tracing for new methods

commit d59880c338eaa8214fe06b4f930a540793d78407
Merge: 0ca4c7c b4f0eb3
Author: Hector Sanjuan <hsanjuan@users.noreply.github.com>
Date:   Wed Feb 13 15:22:49 2019 +0000

    Merge branch 'master' into issue_450

commit 0ca4c7c3b0670ed9c8279f8274d36e3485c10030
Merge: d35017a ecef9ea
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Tue Feb 12 13:10:13 2019 +0530

    Merge branch 'master' into issue_450

commit d35017a8de91ca9fc9a9a047c48c75134cee9f98
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Tue Feb 12 13:07:25 2019 +0530

    PinPath: more improvements

    - Worth having `PinOptions` as a separate field in the struct and
    constructing the query in the test with ToQuery()
    - sharness: "intialization" line can be placed outside the tests at
    the top

commit 68e3b90417ffbad89d41a70ac81d85f9037f8848
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Sun Feb 10 21:43:50 2019 +0530

    Using if-continue pattern instead of if-else

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit 3c29799f3b85be328b27508332ab92049d8b82f3
Merge: 956790b 4324889
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Thu Feb 7 10:25:52 2019 +0530

    Merge branch 'master' into issue_450

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit 956790b381db9858e4194f983e898b07dc51ba66
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Wed Feb 6 21:11:20 2019 +0530

    Removing resolved path

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit 7191cc46cedfbec116a9746937e28881b50ca044
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Wed Feb 6 16:45:07 2019 +0530

    Fix go vet

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit f8b3d5b63b1b7569e2a3e0d82894fd4491c246c4
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Wed Feb 6 16:07:03 2019 +0530

    Fixed linting error

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit 23c57eb467755a1f21387a1615a7f34e97348053
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Wed Feb 6 09:20:41 2019 +0530

    Fixed tests

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit 0caedd94aefeb3b6649dedc214cb4b849ace2ea4
Merge: 17e555e 5a7ee1d
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Wed Feb 6 00:07:10 2019 +0530

    Merge branch 'master' into issue_450

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit 17e555e4a7c574413df90aac70c5cc29cab98f54
Author: Hector Sanjuan <code@hector.link>
Date:   Tue Feb 5 16:58:50 2019 +0000

    PinPath: address some feedback + improvements

    * Changed client's Pin() API and PinPath to be consistent
    * Added helper methods to turn PinPath to query and back
    * Make code and tests build
    * Use TestCidResolved everywhere
    * Fix cluster.PinPath arguments
    * Fix formatting of responses with --no-status
    * Make tests readable and call Fatal when needed
    * Use a pathTestCases variable

commit f0e7369c47c5ddadc8ed45df5fd2d4d9b2d42b38
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Tue Feb 5 18:34:26 2019 +0530

    Support PinPath, UnpinPath(resolve before pinning)

    Addressed review comments as in
    https://github.com/ipfs/ipfs-cluster/pull/634#pullrequestreview-198751932

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit a8b4f181d2d7afed32ee41331dfaab19fd66a173
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Tue Jan 29 22:41:27 2019 +0530

    Fixing tests

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit e39b95ca19e4d75506f4f492678245ef13936a44
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Tue Jan 29 14:52:53 2019 +0530

    Support PinPath, UnpinPath(resolve before pinning)

    - PinPath and UnpinPath should return api.Pin
    - PinPath should accept pin options
    - Removing duplicate logic for Resolve from cluster
    - And many other review comments https://github.com/ipfs/ipfs-cluster/pull/634#pullrequestreview-195509504

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit d146075126320896665ba58d337a13789f68ea86
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Wed Jan 23 17:08:41 2019 +0530

    Support PinPath, UnpinPath(resolve before pinning)

    PinPath(in both rest and rpc) should return a serializable struct in the
    form `{"\":"Q...cid..string..."}` (as used in "github.com/ipfs/go-cid"
    to marshal and unmarshal)

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit 1f4869568a8adb450275257154ea3a26d03a30f3
Merge: 7acfd28 a244af9
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Wed Jan 23 07:18:56 2019 +0530

    Merge branch 'master' into issue_450

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit 7acfd282732ddf2282a67d4f9d0170a494eb3ed4
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Tue Jan 22 18:14:32 2019 +0530

    Support PinPath, UnpinPath(resolve before pinning)

    - RPC must always use serializable structs
    - In command, just use pin with path as cid is also a valid path
    - Addressing many other small review comments as in
    https://github.com/ipfs/ipfs-cluster/pull/634#pullrequestreview-192122534

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit 36905041e1e3f0b204942030aab3ab7b5b9e4d62
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Wed Jan 16 09:36:42 2019 +0530

    Support PinPath, UnpinPath(resolve before pinning)

    Extra logic for path checking should go into resolve so that it can be
    properly reused
    Added sharness tests

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit 9116bda3534e77bb391d873051bb520a1b01a326
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Wed Jan 16 08:08:07 2019 +0530

    Support PinPath, UnpinPath(resolve before pinning)

    error strings should not be capitalized

    Fixes #450

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit ca7e61861374f456300a85ddc0374e594f74f963
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Tue Jan 15 23:40:25 2019 +0530

    Support PinPath, UnpinPath(resolve before pinning)

    Tests

    Fixes #450

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit 522fbcd899f01c01680375561a32a87464157c0a
Merge: f1a56ab f7bc468
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Tue Jan 15 10:40:54 2019 +0530

    Merge branch 'master' into issue_450

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit f1a56ab925fb74c0c44273a4524afa4843cf757f
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Mon Jan 14 20:58:17 2019 +0530

    Support PinPath, UnpinPath(resolve before pinning)

    - IPFS Connector should act as a pure IPFS client, any extra logic
    should go to cluster.go
    - Use cid.Undef, instead of cid.Cid{}

    Fixes #450

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit c83b91054f6774f1f9d4930cfc3f1fa28236f57c
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Thu Jan 10 08:57:17 2019 +0530

    Support PinPath, UnpinPath(resolve before pinning)

    - Separate handlers, methods and rpc apis for PinPath and UnpinPath from
    Pin and Unpin
    - Support ipld paths as well

    Fixes #450

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit 719dff88129366ce3ccb5e04cb6f8082a0915c5c
Merge: 91ceb47 21170c4
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Wed Jan 9 19:38:35 2019 +0530

    Merge branch 'issue_450_old' into HEAD

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit 91ceb4796259ca7ef2974ec43e6a278a12796b13
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Wed Jan 9 19:36:41 2019 +0530

    Revert "WIP: Figure out why test does not impleme"

    This reverts commit 28a3a3f25dce6f296c8cbef86221644c099a7e75.

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

commit 28a3a3f25dce6f296c8cbef86221644c099a7e75
Author: cd10012 <ced361@nyu.edu>
Date:   Tue Jul 24 23:23:10 2018 -0400

    WIP: Figure out why test does not implement IPFSConnector interface...

    License: MIT
    Signed-off-by: cd10012 <ced361@nyu.edu>

commit 21170c48e77e69583db64544b08120a9baf40d8d
Author: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>
Date:   Tue Jan 8 10:37:59 2019 +0530

    Support PinPath, UnpinPath (resolve before pinning)

    This commit adds API support for pinning using path

    `POST /pins/<ipfs or ipns path>` and `DELETE /pins/<ipfs or ipns path>`
    will resolve the path into a cid and perform perform pinning or
    unpinning

    Fixes #450

    License: MIT
    Signed-off-by: Kishan Mohanbhai Sagathiya <kishansagathiya@gmail.com>

Co-authored-by: Hector Sanjuan <hector@protocol.ai>

License: MIT
Signed-off-by: Hector Sanjuan <hector@protocol.ai>
2019-02-20 11:13:03 +00:00

1128 lines
24 KiB
Go

// Package rest implements an IPFS Cluster API component. It provides
// a REST-ish API to interact with Cluster.
//
// rest exposes the HTTP API in two ways. The first is through a regular
// HTTP(s) listener. The second is by tunneling HTTP through a libp2p
// stream (thus getting an encrypted channel without the need to setup
// TLS). Both ways can be used at the same time, or disabled.
package rest
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"math/rand"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/rs/cors"
"go.opencensus.io/plugin/ochttp"
"go.opencensus.io/plugin/ochttp/propagation/tracecontext"
"go.opencensus.io/trace"
"github.com/ipfs/ipfs-cluster/adder/adderutils"
types "github.com/ipfs/ipfs-cluster/api"
mux "github.com/gorilla/mux"
gostream "github.com/hsanjuan/go-libp2p-gostream"
p2phttp "github.com/hsanjuan/go-libp2p-http"
cid "github.com/ipfs/go-cid"
logging "github.com/ipfs/go-log"
gopath "github.com/ipfs/go-path"
libp2p "github.com/libp2p/go-libp2p"
rpc "github.com/libp2p/go-libp2p-gorpc"
host "github.com/libp2p/go-libp2p-host"
peer "github.com/libp2p/go-libp2p-peer"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr-net"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
var logger = logging.Logger("restapi")
// Common errors
var (
// ErrNoEndpointEnabled is returned when the API is created but
// no HTTPListenAddr, nor libp2p configuration fields, nor a libp2p
// Host are provided.
ErrNoEndpointsEnabled = errors.New("neither the libp2p nor the HTTP endpoints are enabled")
// ErrHTTPEndpointNotEnabled is returned when trying to perform
// operations that rely on the HTTPEndpoint but it is disabled.
ErrHTTPEndpointNotEnabled = errors.New("the HTTP endpoint is not enabled")
)
// Used by sendResponse to set the right status
const autoStatus = -1
// For making a random sharding ID
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
// API implements an API and aims to provides
// a RESTful HTTP API for Cluster.
type API struct {
ctx context.Context
cancel func()
config *Config
rpcClient *rpc.Client
rpcReady chan struct{}
router *mux.Router
server *http.Server
host host.Host
httpListener net.Listener
libp2pListener net.Listener
shutdownLock sync.Mutex
shutdown bool
wg sync.WaitGroup
}
type route struct {
Name string
Method string
Pattern string
HandlerFunc http.HandlerFunc
}
type peerAddBody struct {
PeerID string `json:"peer_id"`
}
// NewAPI creates a new REST API component with the given configuration.
func NewAPI(ctx context.Context, cfg *Config) (*API, error) {
return NewAPIWithHost(ctx, cfg, nil)
}
// NewAPIWithHost creates a new REST API component and enables
// the libp2p-http endpoint using the given Host, if not nil.
func NewAPIWithHost(ctx context.Context, cfg *Config, h host.Host) (*API, error) {
err := cfg.Validate()
if err != nil {
return nil, err
}
// Our handler is a gorilla router,
// wrapped with the cors handler,
// wrapped with the basic auth handler.
router := mux.NewRouter().StrictSlash(true)
handler := basicAuthHandler(
cfg.BasicAuthCreds,
cors.New(*cfg.corsOptions()).Handler(router),
)
if cfg.Tracing {
handler = &ochttp.Handler{
IsPublicEndpoint: true,
Propagation: &tracecontext.HTTPFormat{},
Handler: handler,
StartOptions: trace.StartOptions{SpanKind: trace.SpanKindServer},
FormatSpanName: func(req *http.Request) string { return req.Host + ":" + req.URL.Path + ":" + req.Method },
}
}
s := &http.Server{
ReadTimeout: cfg.ReadTimeout,
ReadHeaderTimeout: cfg.ReadHeaderTimeout,
WriteTimeout: cfg.WriteTimeout,
IdleTimeout: cfg.IdleTimeout,
Handler: handler,
}
// See: https://github.com/ipfs/go-ipfs/issues/5168
// See: https://github.com/ipfs/ipfs-cluster/issues/548
// on why this is re-enabled.
s.SetKeepAlivesEnabled(true)
ctx, cancel := context.WithCancel(ctx)
api := &API{
ctx: ctx,
cancel: cancel,
config: cfg,
server: s,
host: h,
rpcReady: make(chan struct{}, 2),
}
api.addRoutes(router)
// Set up api.httpListener if enabled
err = api.setupHTTP(ctx)
if err != nil {
return nil, err
}
// Set up api.libp2pListener if enabled
err = api.setupLibp2p(ctx)
if err != nil {
return nil, err
}
if api.httpListener == nil && api.libp2pListener == nil {
return nil, ErrNoEndpointsEnabled
}
api.run(ctx)
return api, nil
}
func (api *API) setupHTTP(ctx context.Context) error {
if api.config.HTTPListenAddr == nil {
return nil
}
n, addr, err := manet.DialArgs(api.config.HTTPListenAddr)
if err != nil {
return err
}
var l net.Listener
if api.config.TLS != nil {
l, err = tls.Listen(n, addr, api.config.TLS)
} else {
l, err = net.Listen(n, addr)
}
if err != nil {
return err
}
api.httpListener = l
return nil
}
func (api *API) setupLibp2p(ctx context.Context) error {
// Make new host. Override any provided existing one
// if we have config for a custom one.
if api.config.Libp2pListenAddr != nil {
h, err := libp2p.New(
ctx,
libp2p.Identity(api.config.PrivateKey),
libp2p.ListenAddrs([]ma.Multiaddr{api.config.Libp2pListenAddr}...),
)
if err != nil {
return err
}
api.host = h
}
if api.host == nil {
return nil
}
l, err := gostream.Listen(api.host, p2phttp.DefaultP2PProtocol)
if err != nil {
return err
}
api.libp2pListener = l
return nil
}
// HTTPAddress returns the HTTP(s) listening address
// in host:port format. Useful when configured to start
// on a random port (0). Returns error when the HTTP endpoint
// is not enabled.
func (api *API) HTTPAddress() (string, error) {
if api.httpListener == nil {
return "", ErrHTTPEndpointNotEnabled
}
return api.httpListener.Addr().String(), nil
}
// Host returns the libp2p Host used by the API, if any.
// The result is either the host provided during initialization,
// a default Host created with options from the configuration object,
// or nil.
func (api *API) Host() host.Host {
return api.host
}
func (api *API) addRoutes(router *mux.Router) {
for _, route := range api.routes() {
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(
ochttp.WithRouteTag(
http.HandlerFunc(route.HandlerFunc),
"/"+route.Name,
),
)
}
api.router = router
}
// basicAuth wraps a given handler with basic authentication
func basicAuthHandler(credentials map[string]string, h http.Handler) http.Handler {
if credentials == nil {
return h
}
wrap := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
username, password, ok := r.BasicAuth()
if !ok {
resp, err := unauthorizedResp()
if err != nil {
logger.Error(err)
return
}
http.Error(w, resp, 401)
return
}
authorized := false
for u, p := range credentials {
if u == username && p == password {
authorized = true
}
}
if !authorized {
resp, err := unauthorizedResp()
if err != nil {
logger.Error(err)
return
}
http.Error(w, resp, 401)
return
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(wrap)
}
func unauthorizedResp() (string, error) {
apiError := types.Error{
Code: 401,
Message: "Unauthorized",
}
resp, err := json.Marshal(apiError)
return string(resp), err
}
func (api *API) routes() []route {
return []route{
{
"ID",
"GET",
"/id",
api.idHandler,
},
{
"Version",
"GET",
"/version",
api.versionHandler,
},
{
"Peers",
"GET",
"/peers",
api.peerListHandler,
},
{
"PeerAdd",
"POST",
"/peers",
api.peerAddHandler,
},
{
"PeerRemove",
"DELETE",
"/peers/{peer}",
api.peerRemoveHandler,
},
{
"Add",
"POST",
"/add",
api.addHandler,
},
{
"Allocations",
"GET",
"/allocations",
api.allocationsHandler,
},
{
"Allocation",
"GET",
"/allocations/{hash}",
api.allocationHandler,
},
{
"StatusAll",
"GET",
"/pins",
api.statusAllHandler,
},
{
"Sync",
"POST",
"/pins/{hash}/sync",
api.syncHandler,
},
{
"SyncAll",
"POST",
"/pins/sync",
api.syncAllHandler,
},
{
"Recover",
"POST",
"/pins/{hash}/recover",
api.recoverHandler,
},
{
"RecoverAll",
"POST",
"/pins/recover",
api.recoverAllHandler,
},
{
"Status",
"GET",
"/pins/{hash}",
api.statusHandler,
},
{
"Pin",
"POST",
"/pins/{hash}",
api.pinHandler,
},
{
"PinPath",
"POST",
"/pins/{keyType:ipfs|ipns|ipld}/{path:.*}",
api.pinPathHandler,
},
{
"Unpin",
"DELETE",
"/pins/{hash}",
api.unpinHandler,
},
{
"UnpinPath",
"DELETE",
"/pins/{keyType:ipfs|ipns|ipld}/{path:.*}",
api.unpinPathHandler,
},
{
"ConnectionGraph",
"GET",
"/health/graph",
api.graphHandler,
},
{
"Metrics",
"GET",
"/monitor/metrics/{name}",
api.metricsHandler,
},
}
}
func (api *API) run(ctx context.Context) {
if api.httpListener != nil {
api.wg.Add(1)
go api.runHTTPServer(ctx)
}
if api.libp2pListener != nil {
api.wg.Add(1)
go api.runLibp2pServer(ctx)
}
}
// runs in goroutine from run()
func (api *API) runHTTPServer(ctx context.Context) {
defer api.wg.Done()
<-api.rpcReady
logger.Infof("REST API (HTTP): %s", api.config.HTTPListenAddr)
err := api.server.Serve(api.httpListener)
if err != nil && !strings.Contains(err.Error(), "closed network connection") {
logger.Error(err)
}
}
// runs in goroutine from run()
func (api *API) runLibp2pServer(ctx context.Context) {
defer api.wg.Done()
<-api.rpcReady
listenMsg := ""
for _, a := range api.host.Addrs() {
listenMsg += fmt.Sprintf(" %s/ipfs/%s\n", a, api.host.ID().Pretty())
}
logger.Infof("REST API (libp2p-http): ENABLED. Listening on:\n%s\n", listenMsg)
err := api.server.Serve(api.libp2pListener)
if err != nil && !strings.Contains(err.Error(), "context canceled") {
logger.Error(err)
}
}
// Shutdown stops any API listeners.
func (api *API) Shutdown(ctx context.Context) error {
_, span := trace.StartSpan(ctx, "restapi/Shutdown")
defer span.End()
api.shutdownLock.Lock()
defer api.shutdownLock.Unlock()
if api.shutdown {
logger.Debug("already shutdown")
return nil
}
logger.Info("stopping Cluster API")
api.cancel()
close(api.rpcReady)
// Cancel any outstanding ops
api.server.SetKeepAlivesEnabled(false)
if api.httpListener != nil {
api.httpListener.Close()
}
if api.libp2pListener != nil {
api.libp2pListener.Close()
}
// This means we created the host
if api.config.Libp2pListenAddr != nil {
api.host.Close()
}
api.wg.Wait()
api.shutdown = true
return nil
}
// SetClient makes the component ready to perform RPC
// requests.
func (api *API) SetClient(c *rpc.Client) {
api.rpcClient = c
// One notification for http server and one for libp2p server.
api.rpcReady <- struct{}{}
api.rpcReady <- struct{}{}
}
func (api *API) idHandler(w http.ResponseWriter, r *http.Request) {
idSerial := types.IDSerial{}
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"ID",
struct{}{},
&idSerial,
)
api.sendResponse(w, autoStatus, err, idSerial)
}
func (api *API) versionHandler(w http.ResponseWriter, r *http.Request) {
var v types.Version
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"Version",
struct{}{},
&v,
)
api.sendResponse(w, autoStatus, err, v)
}
func (api *API) graphHandler(w http.ResponseWriter, r *http.Request) {
var graph types.ConnectGraphSerial
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"ConnectGraph",
struct{}{},
&graph,
)
api.sendResponse(w, autoStatus, err, graph)
}
func (api *API) metricsHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name := vars["name"]
var metrics []types.Metric
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"PeerMonitorLatestMetrics",
name,
&metrics,
)
api.sendResponse(w, autoStatus, err, metrics)
}
func (api *API) addHandler(w http.ResponseWriter, r *http.Request) {
reader, err := r.MultipartReader()
if err != nil {
api.sendResponse(w, http.StatusBadRequest, err, nil)
return
}
params, err := types.AddParamsFromQuery(r.URL.Query())
if err != nil {
api.sendResponse(w, http.StatusBadRequest, err, nil)
return
}
api.setHeaders(w)
// any errors sent as trailer
adderutils.AddMultipartHTTPHandler(
r.Context(),
api.rpcClient,
params,
reader,
w,
nil,
)
return
}
func (api *API) peerListHandler(w http.ResponseWriter, r *http.Request) {
var peersSerial []types.IDSerial
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"Peers",
struct{}{},
&peersSerial,
)
api.sendResponse(w, autoStatus, err, peersSerial)
}
func (api *API) peerAddHandler(w http.ResponseWriter, r *http.Request) {
dec := json.NewDecoder(r.Body)
defer r.Body.Close()
var addInfo peerAddBody
err := dec.Decode(&addInfo)
if err != nil {
api.sendResponse(w, http.StatusBadRequest, errors.New("error decoding request body"), nil)
return
}
_, err = peer.IDB58Decode(addInfo.PeerID)
if err != nil {
api.sendResponse(w, http.StatusBadRequest, errors.New("error decoding peer_id"), nil)
return
}
var ids types.IDSerial
err = api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"PeerAdd",
addInfo.PeerID,
&ids,
)
api.sendResponse(w, autoStatus, err, ids)
}
func (api *API) peerRemoveHandler(w http.ResponseWriter, r *http.Request) {
if p := api.parsePidOrError(w, r); p != "" {
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"PeerRemove",
p,
&struct{}{},
)
api.sendResponse(w, autoStatus, err, nil)
}
}
func (api *API) pinHandler(w http.ResponseWriter, r *http.Request) {
if ps := api.parseCidOrError(w, r); ps.Cid != "" {
logger.Debugf("rest api pinHandler: %s", ps.Cid)
// span.AddAttributes(trace.StringAttribute("cid", ps.Cid))
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"Pin",
ps,
&struct{}{},
)
api.sendResponse(w, http.StatusAccepted, err, nil)
logger.Debug("rest api pinHandler done")
}
}
func (api *API) unpinHandler(w http.ResponseWriter, r *http.Request) {
if ps := api.parseCidOrError(w, r); ps.Cid != "" {
logger.Debugf("rest api unpinHandler: %s", ps.Cid)
// span.AddAttributes(trace.StringAttribute("cid", ps.Cid))
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"Unpin",
ps,
&struct{}{},
)
api.sendResponse(w, http.StatusAccepted, err, nil)
logger.Debug("rest api unpinHandler done")
}
}
func (api *API) pinPathHandler(w http.ResponseWriter, r *http.Request) {
var pin types.PinSerial
if pinpath := api.parsePinPathOrError(w, r); pinpath.Path != "" {
logger.Debugf("rest api pinPathHandler: %s", pinpath.Path)
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"PinPath",
pinpath,
&pin,
)
api.sendResponse(w, http.StatusOK, err, pin)
logger.Debug("rest api pinPathHandler done")
}
}
func (api *API) unpinPathHandler(w http.ResponseWriter, r *http.Request) {
var pin types.PinSerial
if pinpath := api.parsePinPathOrError(w, r); pinpath.Path != "" {
logger.Debugf("rest api unpinPathHandler: %s", pinpath.Path)
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"UnpinPath",
pinpath.Path,
&pin,
)
api.sendResponse(w, http.StatusOK, err, pin)
logger.Debug("rest api unpinPathHandler done")
}
}
func (api *API) allocationsHandler(w http.ResponseWriter, r *http.Request) {
queryValues := r.URL.Query()
filterStr := queryValues.Get("filter")
var filter types.PinType
for _, f := range strings.Split(filterStr, ",") {
filter |= types.PinTypeFromString(f)
}
var pins []types.PinSerial
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"Pins",
struct{}{},
&pins,
)
outPins := make([]types.PinSerial, 0)
for _, pinS := range pins {
if uint64(filter)&pinS.Type > 0 {
// add this pin to output
outPins = append(outPins, pinS)
}
}
api.sendResponse(w, autoStatus, err, outPins)
}
func (api *API) allocationHandler(w http.ResponseWriter, r *http.Request) {
if ps := api.parseCidOrError(w, r); ps.Cid != "" {
var pin types.PinSerial
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"PinGet",
ps,
&pin,
)
if err != nil { // errors here are 404s
api.sendResponse(w, http.StatusNotFound, err, nil)
return
}
api.sendResponse(w, autoStatus, nil, pin)
}
}
// filterGlobalPinInfos takes a GlobalPinInfo slice and discards
// any item in it which does not carry a PinInfo matching the
// filter (OR-wise).
func filterGlobalPinInfos(globalPinInfos []types.GlobalPinInfoSerial, filter types.TrackerStatus) []types.GlobalPinInfoSerial {
if filter == types.TrackerStatusUndefined {
return globalPinInfos
}
var filteredGlobalPinInfos []types.GlobalPinInfoSerial
for _, globalPinInfo := range globalPinInfos {
for _, pinInfo := range globalPinInfo.PeerMap {
st := types.TrackerStatusFromString(pinInfo.Status)
// silenced the error because we should have detected earlier if filters were invalid
if st.Match(filter) {
filteredGlobalPinInfos = append(filteredGlobalPinInfos, globalPinInfo)
break
}
}
}
return filteredGlobalPinInfos
}
func (api *API) statusAllHandler(w http.ResponseWriter, r *http.Request) {
queryValues := r.URL.Query()
local := queryValues.Get("local")
var globalPinInfos []types.GlobalPinInfoSerial
filterStr := queryValues.Get("filter")
filter := types.TrackerStatusFromString(filterStr)
if filter == types.TrackerStatusUndefined && filterStr != "" {
api.sendResponse(w, autoStatus, errors.New("invalid filter value"), nil)
return
}
if local == "true" {
var pinInfos []types.PinInfoSerial
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"StatusAllLocal",
struct{}{},
&pinInfos,
)
if err != nil {
api.sendResponse(w, autoStatus, err, nil)
return
}
globalPinInfos = pinInfosToGlobal(pinInfos)
} else {
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"StatusAll",
struct{}{},
&globalPinInfos,
)
if err != nil {
api.sendResponse(w, autoStatus, err, nil)
return
}
}
globalPinInfos = filterGlobalPinInfos(globalPinInfos, filter)
api.sendResponse(w, autoStatus, nil, globalPinInfos)
}
func (api *API) statusHandler(w http.ResponseWriter, r *http.Request) {
queryValues := r.URL.Query()
local := queryValues.Get("local")
if ps := api.parseCidOrError(w, r); ps.Cid != "" {
if local == "true" {
var pinInfo types.PinInfoSerial
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"StatusLocal",
ps,
&pinInfo,
)
api.sendResponse(w, autoStatus, err, pinInfoToGlobal(pinInfo))
} else {
var pinInfo types.GlobalPinInfoSerial
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"Status",
ps,
&pinInfo,
)
api.sendResponse(w, autoStatus, err, pinInfo)
}
}
}
func (api *API) syncAllHandler(w http.ResponseWriter, r *http.Request) {
queryValues := r.URL.Query()
local := queryValues.Get("local")
if local == "true" {
var pinInfos []types.PinInfoSerial
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"SyncAllLocal",
struct{}{},
&pinInfos,
)
api.sendResponse(w, autoStatus, err, pinInfosToGlobal(pinInfos))
} else {
var pinInfos []types.GlobalPinInfoSerial
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"SyncAll",
struct{}{},
&pinInfos,
)
api.sendResponse(w, autoStatus, err, pinInfos)
}
}
func (api *API) syncHandler(w http.ResponseWriter, r *http.Request) {
queryValues := r.URL.Query()
local := queryValues.Get("local")
if ps := api.parseCidOrError(w, r); ps.Cid != "" {
if local == "true" {
var pinInfo types.PinInfoSerial
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"SyncLocal",
ps,
&pinInfo,
)
api.sendResponse(w, autoStatus, err, pinInfoToGlobal(pinInfo))
} else {
var pinInfo types.GlobalPinInfoSerial
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"Sync",
ps,
&pinInfo,
)
api.sendResponse(w, autoStatus, err, pinInfo)
}
}
}
func (api *API) recoverAllHandler(w http.ResponseWriter, r *http.Request) {
queryValues := r.URL.Query()
local := queryValues.Get("local")
if local == "true" {
var pinInfos []types.PinInfoSerial
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"RecoverAllLocal",
struct{}{},
&pinInfos,
)
api.sendResponse(w, autoStatus, err, pinInfosToGlobal(pinInfos))
} else {
api.sendResponse(w, http.StatusBadRequest, errors.New("only requests with parameter local=true are supported"), nil)
}
}
func (api *API) recoverHandler(w http.ResponseWriter, r *http.Request) {
queryValues := r.URL.Query()
local := queryValues.Get("local")
if ps := api.parseCidOrError(w, r); ps.Cid != "" {
if local == "true" {
var pinInfo types.PinInfoSerial
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"RecoverLocal",
ps,
&pinInfo,
)
api.sendResponse(w, autoStatus, err, pinInfoToGlobal(pinInfo))
} else {
var pinInfo types.GlobalPinInfoSerial
err := api.rpcClient.CallContext(
r.Context(),
"",
"Cluster",
"Recover",
ps,
&pinInfo,
)
api.sendResponse(w, autoStatus, err, pinInfo)
}
}
}
func (api *API) parsePinPathOrError(w http.ResponseWriter, r *http.Request) types.PinPath {
vars := mux.Vars(r)
urlpath := "/" + vars["keyType"] + "/" + strings.TrimSuffix(vars["path"], "/")
path, err := gopath.ParsePath(urlpath)
if err != nil {
api.sendResponse(w, http.StatusBadRequest, errors.New("error parsing path: "+err.Error()), nil)
return types.PinPath{}
}
pinPath := types.PinPath{Path: path.String()}
pinPath.PinOptions.FromQuery(r.URL.Query())
return pinPath
}
func (api *API) parseCidOrError(w http.ResponseWriter, r *http.Request) types.PinSerial {
vars := mux.Vars(r)
hash := vars["hash"]
_, err := cid.Decode(hash)
if err != nil {
api.sendResponse(w, http.StatusBadRequest, errors.New("error decoding Cid: "+err.Error()), nil)
return types.PinSerial{Cid: ""}
}
pin := types.PinSerial{
Cid: hash,
Type: uint64(types.DataType),
}
pin.PinOptions.FromQuery(r.URL.Query())
pin.MaxDepth = -1 // For now, all pins are recursive
return pin
}
func (api *API) parsePidOrError(w http.ResponseWriter, r *http.Request) peer.ID {
vars := mux.Vars(r)
idStr := vars["peer"]
pid, err := peer.IDB58Decode(idStr)
if err != nil {
api.sendResponse(w, http.StatusBadRequest, errors.New("error decoding Peer ID: "+err.Error()), nil)
return ""
}
return pid
}
func pinInfoToGlobal(pInfo types.PinInfoSerial) types.GlobalPinInfoSerial {
return types.GlobalPinInfoSerial{
Cid: pInfo.Cid,
PeerMap: map[string]types.PinInfoSerial{
pInfo.Peer: pInfo,
},
}
}
func pinInfosToGlobal(pInfos []types.PinInfoSerial) []types.GlobalPinInfoSerial {
gPInfos := make([]types.GlobalPinInfoSerial, len(pInfos), len(pInfos))
for i, p := range pInfos {
gPInfos[i] = pinInfoToGlobal(p)
}
return gPInfos
}
// sendResponse wraps all the logic for writing the response to a request:
// * Write configured headers
// * Write application/json content type
// * Write status: determined automatically if given "autoStatus"
// * Write an error if there is or write the response if there is
func (api *API) sendResponse(
w http.ResponseWriter,
status int,
err error,
resp interface{},
) {
api.setHeaders(w)
enc := json.NewEncoder(w)
// Send an error
if err != nil {
if status == autoStatus || status < 400 { // set a default error status
status = http.StatusInternalServerError
}
w.WriteHeader(status)
errorResp := types.Error{
Code: status,
Message: err.Error(),
}
logger.Errorf("sending error response: %d: %s", status, err.Error())
if err := enc.Encode(errorResp); err != nil {
logger.Error(err)
}
return
}
// Send a body
if resp != nil {
if status == autoStatus {
status = http.StatusOK
}
w.WriteHeader(status)
if err = enc.Encode(resp); err != nil {
logger.Error(err)
}
return
}
// Empty response
if status == autoStatus {
status = http.StatusNoContent
}
w.WriteHeader(status)
}
// this sets all the headers that are common to all responses
// from this API. Called from sendResponse() and /add.
func (api *API) setHeaders(w http.ResponseWriter) {
for header, values := range api.config.Headers {
for _, val := range values {
w.Header().Add(header, val)
}
}
w.Header().Add("Content-Type", "application/json")
}