Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package client

import (
"context"
"net/http"
"net/url"
"path"

"github.com/filecoin-project/go-jsonrpc"

"github.com/filecoin-project/boost/api"
"github.com/filecoin-project/boost/lib/rpcenc"
)

func getPushUrl(addr string) (string, error) {
pushUrl, err := url.Parse(addr)
if err != nil {
return "", err
}
switch pushUrl.Scheme {
case "ws":
pushUrl.Scheme = "http"
case "wss":
pushUrl.Scheme = "https"
}
///rpc/v0 -> /rpc/streams/v0/push

pushUrl.Path = path.Join(pushUrl.Path, "../streams/v0/push")
return pushUrl.String(), nil
}

// NewBoostRPCV0 creates a new http jsonrpc client for miner
func NewBoostRPCV0(ctx context.Context, addr string, requestHeader http.Header, opts ...jsonrpc.Option) (api.Boost, jsonrpc.ClientCloser, error) {
pushUrl, err := getPushUrl(addr)
if err != nil {
return nil, nil, err
}

var res api.BoostStruct
closer, err := jsonrpc.NewMergeClient(ctx, addr, "Filecoin",
api.GetInternalStructs(&res), requestHeader,
append([]jsonrpc.Option{
rpcenc.ReaderParamEncoder(pushUrl),
}, opts...)...)

return &res, closer, err
}
9 changes: 9 additions & 0 deletions cli/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package cli

import (
cliutil "github.com/filecoin-project/boost/cli/util"
)

var GetBoostAPI = cliutil.GetBoostAPI

var DaemonContext = cliutil.DaemonContext
250 changes: 250 additions & 0 deletions cli/util/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
package cliutil

import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
"syscall"

"github.com/mitchellh/go-homedir"
"github.com/urfave/cli/v2"
"golang.org/x/xerrors"

"github.com/filecoin-project/go-jsonrpc"

logging "github.com/ipfs/go-log/v2"

"github.com/filecoin-project/boost/api"
"github.com/filecoin-project/boost/api/client"
"github.com/filecoin-project/boost/node/repo"
)

const (
metadataTraceContext = "traceContext"
)

var log = logging.Logger("cli")

// flagsForAPI returns flags passed on the command line with the listen address
// of the API server (only used by the tests), in the order of precedence they
// should be applied for the requested kind of node.
func flagsForAPI(t repo.RepoType) []string {
switch t {
case repo.Boost:
return []string{"boost-api-url"}
default:
panic(fmt.Sprintf("Unknown repo type: %v", t))
}
}

func flagsForRepo(t repo.RepoType) []string {
switch t {
case repo.Boost:
return []string{"boost-repo"}
default:
panic(fmt.Sprintf("Unknown repo type: %v", t))
}
}

// EnvsForAPIInfos returns the environment variables to use in order of precedence
// to determine the API endpoint of the specified node type.
//
// It returns the current variables and deprecated ones separately, so that
// the user can log a warning when deprecated ones are found to be in use.
func EnvsForAPIInfos(t repo.RepoType) (primary string, fallbacks []string, deprecated []string) {
switch t {
case repo.Boost:
return "BOOST_API_INFO", []string{"BOOST_API_INFO"}, nil
default:
panic(fmt.Sprintf("Unknown repo type: %v", t))
}
}

// GetAPIInfo returns the API endpoint to use for the specified kind of repo.
//
// The order of precedence is as follows:
//
// 1. *-api-url command line flags.
// 2. *_API_INFO environment variables
// 3. deprecated *_API_INFO environment variables
// 4. *-repo command line flags.
Comment on lines +72 to +75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need all of these? It might be simpler to just support one of them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need them in some form - we need to configure boost and give the location of the service when interacting with it.

I think keeping BOOST_API_INFO with the Lotus convention is OK.

func GetAPIInfo(ctx *cli.Context, t repo.RepoType) (APIInfo, error) {
// Check if there was a flag passed with the listen address of the API
// server (only used by the tests)
apiFlags := flagsForAPI(t)
for _, f := range apiFlags {
if !ctx.IsSet(f) {
continue
}
strma := ctx.String(f)
strma = strings.TrimSpace(strma)

return APIInfo{Addr: strma}, nil
}

//
// Note: it is not correct/intuitive to prefer environment variables over
// CLI flags (repo flags below).
//
primaryEnv, fallbacksEnvs, deprecatedEnvs := EnvsForAPIInfos(t)
env, ok := os.LookupEnv(primaryEnv)
if ok {
return ParseApiInfo(env), nil
}

for _, env := range deprecatedEnvs {
env, ok := os.LookupEnv(env)
if ok {
log.Warnf("Using deprecated env(%s) value, please use env(%s) instead.", env, primaryEnv)
return ParseApiInfo(env), nil
}
}

repoFlags := flagsForRepo(t)
for _, f := range repoFlags {
// cannot use ctx.IsSet because it ignores default values
path := ctx.String(f)
if path == "" {
continue
}

p, err := homedir.Expand(path)
if err != nil {
return APIInfo{}, xerrors.Errorf("could not expand home dir (%s): %w", f, err)
}

r, err := repo.NewFS(p)
if err != nil {
return APIInfo{}, xerrors.Errorf("could not open repo at path: %s; %w", p, err)
}

exists, err := r.Exists()
if err != nil {
return APIInfo{}, xerrors.Errorf("repo.Exists returned an error: %w", err)
}

if !exists {
return APIInfo{}, errors.New("repo directory does not exist. Make sure your configuration is correct")
}

ma, err := r.APIEndpoint()
if err != nil {
return APIInfo{}, xerrors.Errorf("could not get api endpoint: %w", err)
}

token, err := r.APIToken()
if err != nil {
log.Warnf("Couldn't load CLI token, capabilities may be limited: %v", err)
}

return APIInfo{
Addr: ma.String(),
Token: token,
}, nil
}

for _, env := range fallbacksEnvs {
env, ok := os.LookupEnv(env)
if ok {
return ParseApiInfo(env), nil
}
}

return APIInfo{}, fmt.Errorf("could not determine API endpoint for node type: %v", t)
}

type GetBoostOptions struct {
PreferHttp bool
}

type GetBoostOption func(*GetBoostOptions)

func BoostUseHttp(opts *GetBoostOptions) {
opts.PreferHttp = true
}

func GetBoostAPI(ctx *cli.Context, opts ...GetBoostOption) (api.Boost, jsonrpc.ClientCloser, error) {
var options GetBoostOptions
for _, opt := range opts {
opt(&options)
}

if tn, ok := ctx.App.Metadata["testnode-boost"]; ok {
return tn.(api.Boost), func() {}, nil
}

addr, headers, err := GetRawAPI(ctx, repo.Boost, "v0")
if err != nil {
return nil, nil, err
}

if options.PreferHttp {
u, err := url.Parse(addr)
if err != nil {
return nil, nil, xerrors.Errorf("parsing miner api URL: %w", err)
}

switch u.Scheme {
case "ws":
u.Scheme = "http"
case "wss":
u.Scheme = "https"
}

addr = u.String()
}

if IsVeryVerbose {
_, _ = fmt.Fprintln(ctx.App.Writer, "using miner API v0 endpoint:", addr)
}

return client.NewBoostRPCV0(ctx.Context, addr, headers)
}

func GetRawAPI(ctx *cli.Context, t repo.RepoType, version string) (string, http.Header, error) {
ainfo, err := GetAPIInfo(ctx, t)
if err != nil {
return "", nil, xerrors.Errorf("could not get API info for %s: %w", t, err)
}

addr, err := ainfo.DialArgs(version)
if err != nil {
return "", nil, xerrors.Errorf("could not get DialArgs: %w", err)
}

if IsVeryVerbose {
_, _ = fmt.Fprintf(ctx.App.Writer, "using raw API %s endpoint: %s\n", version, addr)
}

return addr, ainfo.AuthHeader(), nil
}

func DaemonContext(cctx *cli.Context) context.Context {
if mtCtx, ok := cctx.App.Metadata[metadataTraceContext]; ok {
return mtCtx.(context.Context)
}

return context.Background()
}

// ReqContext returns context for cli execution. Calling it for the first time
// installs SIGTERM handler that will close returned context.
// Not safe for concurrent execution.
func ReqContext(cctx *cli.Context) context.Context {
tCtx := DaemonContext(cctx)

ctx, done := context.WithCancel(tCtx)
sigChan := make(chan os.Signal, 2)
go func() {
<-sigChan
done()
}()
signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP)

return ctx
}
80 changes: 80 additions & 0 deletions cli/util/apiinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package cliutil

import (
"net/http"
"net/url"
"regexp"
"strings"

"github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
)

var (
infoWithToken = regexp.MustCompile(`^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?:.+$`)
)

type APIInfo struct {
Addr string
Token []byte
}

func ParseApiInfo(s string) APIInfo {
var tok []byte
if infoWithToken.Match([]byte(s)) {
sp := strings.SplitN(s, ":", 2)
tok = []byte(sp[0])
s = sp[1]
}

return APIInfo{
Addr: s,
Token: tok,
}
}

func (a APIInfo) DialArgs(version string) (string, error) {
ma, err := multiaddr.NewMultiaddr(a.Addr)
if err == nil {
_, addr, err := manet.DialArgs(ma)
if err != nil {
return "", err
}

return "ws://" + addr + "/rpc/" + version, nil
}

_, err = url.Parse(a.Addr)
if err != nil {
return "", err
}
return a.Addr + "/rpc/" + version, nil
}

func (a APIInfo) Host() (string, error) {
ma, err := multiaddr.NewMultiaddr(a.Addr)
if err == nil {
_, addr, err := manet.DialArgs(ma)
if err != nil {
return "", err
}

return addr, nil
}

spec, err := url.Parse(a.Addr)
if err != nil {
return "", err
}
return spec.Host, nil
}

func (a APIInfo) AuthHeader() http.Header {
if len(a.Token) != 0 {
headers := http.Header{}
headers.Add("Authorization", "Bearer "+string(a.Token))
return headers
}
log.Warn("API Token not set and requested, capabilities might be limited.")
return nil
}
Loading