commit
c394e3c2aa
@ -33,6 +33,10 @@ type MsgWait struct {
|
||||
|
||||
// API is a low-level interface to the Filecoin network
|
||||
type API interface {
|
||||
// Auth
|
||||
AuthVerify(ctx context.Context, token string) ([]string, error)
|
||||
AuthNew(ctx context.Context, perms []string) ([]byte, error)
|
||||
|
||||
// chain
|
||||
|
||||
ChainHead(context.Context) (*chain.TipSet, error) // TODO: check serialization
|
||||
|
@ -1,13 +1,15 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/filecoin-project/go-lotus/api"
|
||||
"github.com/filecoin-project/go-lotus/lib/jsonrpc"
|
||||
)
|
||||
|
||||
// NewRPC creates a new http jsonrpc client.
|
||||
func NewRPC(addr string) (api.API, error) {
|
||||
func NewRPC(addr string, requestHeader http.Header) (api.API, error) {
|
||||
var res api.Struct
|
||||
_, err := jsonrpc.NewClient(addr, "Filecoin", &res.Internal)
|
||||
_, err := jsonrpc.NewClient(addr, "Filecoin", &res.Internal, requestHeader)
|
||||
return &res, err
|
||||
}
|
||||
|
86
api/permissioned.go
Normal file
86
api/permissioned.go
Normal file
@ -0,0 +1,86 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
type permKey int
|
||||
|
||||
var permCtxKey permKey
|
||||
|
||||
const (
|
||||
// When changing these, update docs/API.md too
|
||||
|
||||
PermRead = "read" // default
|
||||
PermWrite = "write"
|
||||
PermSign = "sign" // Use wallet keys for signing
|
||||
PermAdmin = "admin" // Manage permissions
|
||||
)
|
||||
|
||||
var AllPermissions = []string{PermRead, PermWrite, PermSign, PermAdmin}
|
||||
var defaultPerms = []string{PermRead}
|
||||
|
||||
func WithPerm(ctx context.Context, perms []string) context.Context {
|
||||
return context.WithValue(ctx, permCtxKey, perms)
|
||||
}
|
||||
|
||||
func Permissioned(a API) API {
|
||||
var out Struct
|
||||
|
||||
rint := reflect.ValueOf(&out.Internal).Elem()
|
||||
ra := reflect.ValueOf(a)
|
||||
|
||||
for f := 0; f < rint.NumField(); f++ {
|
||||
field := rint.Type().Field(f)
|
||||
requiredPerm := field.Tag.Get("perm")
|
||||
if requiredPerm == "" {
|
||||
panic("missing 'perm' tag on " + field.Name)
|
||||
}
|
||||
|
||||
// Validate perm tag
|
||||
ok := false
|
||||
for _, perm := range AllPermissions {
|
||||
if requiredPerm == perm {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
panic("unknown 'perm' tag on " + field.Name)
|
||||
}
|
||||
|
||||
fn := ra.MethodByName(field.Name)
|
||||
|
||||
rint.Field(f).Set(reflect.MakeFunc(field.Type, func(args []reflect.Value) (results []reflect.Value) {
|
||||
ctx := args[0].Interface().(context.Context)
|
||||
callerPerms, ok := ctx.Value(permCtxKey).([]string)
|
||||
if !ok {
|
||||
callerPerms = defaultPerms
|
||||
}
|
||||
|
||||
for _, callerPerm := range callerPerms {
|
||||
if callerPerm == requiredPerm {
|
||||
return fn.Call(args)
|
||||
}
|
||||
}
|
||||
|
||||
err := xerrors.Errorf("missing permission to invoke '%s' (need '%s')", field.Name, requiredPerm)
|
||||
rerr := reflect.ValueOf(&err).Elem()
|
||||
|
||||
if field.Type.NumOut() == 2 {
|
||||
return []reflect.Value{
|
||||
reflect.Zero(field.Type.Out(0)),
|
||||
rerr,
|
||||
}
|
||||
} else {
|
||||
return []reflect.Value{rerr}
|
||||
}
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
return &out
|
||||
}
|
@ -11,41 +11,55 @@ import (
|
||||
"github.com/libp2p/go-libp2p-core/peer"
|
||||
)
|
||||
|
||||
// All permissions are listed in permissioned.go
|
||||
var _ = AllPermissions
|
||||
|
||||
// Struct implements API passing calls to user-provided function values.
|
||||
type Struct struct {
|
||||
Internal struct {
|
||||
ID func(context.Context) (peer.ID, error)
|
||||
Version func(context.Context) (Version, error)
|
||||
AuthVerify func(ctx context.Context, token string) ([]string, error) `perm:"read"`
|
||||
AuthNew func(ctx context.Context, perms []string) ([]byte, error) `perm:"admin"`
|
||||
|
||||
ChainSubmitBlock func(ctx context.Context, blk *chain.BlockMsg) error
|
||||
ChainHead func(context.Context) (*chain.TipSet, error)
|
||||
ChainGetRandomness func(context.Context, *chain.TipSet) ([]byte, error)
|
||||
ChainWaitMsg func(context.Context, cid.Cid) (*MsgWait, error)
|
||||
ChainGetBlock func(context.Context, cid.Cid) (*chain.BlockHeader, error)
|
||||
ChainGetBlockMessages func(context.Context, cid.Cid) ([]*chain.SignedMessage, error)
|
||||
ID func(context.Context) (peer.ID, error) `perm:"read"`
|
||||
Version func(context.Context) (Version, error) `perm:"read"`
|
||||
|
||||
MpoolPending func(context.Context, *chain.TipSet) ([]*chain.SignedMessage, error)
|
||||
MpoolPush func(context.Context, *chain.SignedMessage) error
|
||||
ChainSubmitBlock func(ctx context.Context, blk *chain.BlockMsg) error `perm:"write"`
|
||||
ChainHead func(context.Context) (*chain.TipSet, error) `perm:"read"`
|
||||
ChainGetRandomness func(context.Context, *chain.TipSet) ([]byte, error) `perm:"read"`
|
||||
ChainWaitMsg func(context.Context, cid.Cid) (*MsgWait, error) `perm:"read"`
|
||||
ChainGetBlock func(context.Context, cid.Cid) (*chain.BlockHeader, error) `perm:"read"`
|
||||
ChainGetBlockMessages func(context.Context, cid.Cid) ([]*chain.SignedMessage, error) `perm:"read"`
|
||||
|
||||
MinerStart func(context.Context, address.Address) error
|
||||
MinerCreateBlock func(context.Context, address.Address, *chain.TipSet, []chain.Ticket, chain.ElectionProof, []*chain.SignedMessage) (*chain.BlockMsg, error)
|
||||
MpoolPending func(context.Context, *chain.TipSet) ([]*chain.SignedMessage, error) `perm:"read"`
|
||||
MpoolPush func(context.Context, *chain.SignedMessage) error `perm:"write"`
|
||||
|
||||
WalletNew func(context.Context, string) (address.Address, error)
|
||||
WalletList func(context.Context) ([]address.Address, error)
|
||||
WalletBalance func(context.Context, address.Address) (types.BigInt, error)
|
||||
WalletSign func(context.Context, address.Address, []byte) (*chain.Signature, error)
|
||||
WalletDefaultAddress func(context.Context) (address.Address, error)
|
||||
MpoolGetNonce func(context.Context, address.Address) (uint64, error)
|
||||
MinerStart func(context.Context, address.Address) error `perm:"admin"`
|
||||
MinerCreateBlock func(context.Context, address.Address, *chain.TipSet, []chain.Ticket, chain.ElectionProof, []*chain.SignedMessage) (*chain.BlockMsg, error) `perm:"write"`
|
||||
|
||||
ClientImport func(ctx context.Context, path string) (cid.Cid, error)
|
||||
ClientListImports func(ctx context.Context) ([]Import, error)
|
||||
WalletNew func(context.Context, string) (address.Address, error) `perm:"write"`
|
||||
WalletList func(context.Context) ([]address.Address, error) `perm:"write"`
|
||||
WalletBalance func(context.Context, address.Address) (types.BigInt, error) `perm:"read"`
|
||||
WalletSign func(context.Context, address.Address, []byte) (*chain.Signature, error) `perm:"sign"`
|
||||
WalletDefaultAddress func(context.Context) (address.Address, error) `perm:"write"`
|
||||
MpoolGetNonce func(context.Context, address.Address) (uint64, error) `perm:"read"`
|
||||
|
||||
NetPeers func(context.Context) ([]peer.AddrInfo, error)
|
||||
NetConnect func(context.Context, peer.AddrInfo) error
|
||||
NetAddrsListen func(context.Context) (peer.AddrInfo, error)
|
||||
ClientImport func(ctx context.Context, path string) (cid.Cid, error) `perm:"write"`
|
||||
ClientListImports func(ctx context.Context) ([]Import, error) `perm:"read"`
|
||||
|
||||
NetPeers func(context.Context) ([]peer.AddrInfo, error) `perm:"read"`
|
||||
NetConnect func(context.Context, peer.AddrInfo) error `perm:"write"`
|
||||
NetAddrsListen func(context.Context) (peer.AddrInfo, error) `perm:"read"`
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Struct) AuthVerify(ctx context.Context, token string) ([]string, error) {
|
||||
return c.Internal.AuthVerify(ctx, token)
|
||||
}
|
||||
|
||||
func (c *Struct) AuthNew(ctx context.Context, perms []string) ([]byte, error) {
|
||||
return c.Internal.AuthNew(ctx, perms)
|
||||
}
|
||||
|
||||
func (c *Struct) ClientListImports(ctx context.Context) ([]Import, error) {
|
||||
return c.Internal.ClientListImports(ctx)
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
package types
|
||||
|
||||
// KeyInfo is used for storying keys in KeyStore
|
||||
// KeyInfo is used for storing keys in KeyStore
|
||||
type KeyInfo struct {
|
||||
Type string
|
||||
PrivateKey []byte
|
||||
}
|
||||
|
||||
// KeyStore is used for storying secret keys
|
||||
// KeyStore is used for storing secret keys
|
||||
type KeyStore interface {
|
||||
// List lists all the keys stored in the KeyStore
|
||||
List() ([]string, error)
|
||||
// Get gets a key out of keystore and returns KeyInfo coresponding to named key
|
||||
// Get gets a key out of keystore and returns KeyInfo corresponding to named key
|
||||
Get(string) (KeyInfo, error)
|
||||
// Put saves a key info under given name
|
||||
Put(string, KeyInfo) error
|
||||
|
15
cli/cmd.go
15
cli/cmd.go
@ -2,10 +2,12 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
logging "github.com/ipfs/go-log"
|
||||
manet "github.com/multiformats/go-multiaddr-net"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
@ -14,6 +16,8 @@ import (
|
||||
"github.com/filecoin-project/go-lotus/node/repo"
|
||||
)
|
||||
|
||||
var log = logging.Logger("cli")
|
||||
|
||||
const (
|
||||
metadataContext = "context"
|
||||
)
|
||||
@ -35,7 +39,16 @@ func getAPI(ctx *cli.Context) (api.API, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.NewRPC("ws://" + addr + "/rpc/v0")
|
||||
var headers http.Header
|
||||
token, err := r.APIToken()
|
||||
if err != nil {
|
||||
log.Warnf("Couldn't load CLI token, capabilities may be limited: %w", err)
|
||||
} else {
|
||||
headers = http.Header{}
|
||||
headers.Add("Authorization", "Bearer "+string(token))
|
||||
}
|
||||
|
||||
return client.NewRPC("ws://"+addr+"/rpc/v0", headers)
|
||||
}
|
||||
|
||||
// reqContext returns context for cli execution. Calling it for the first time
|
||||
|
@ -49,7 +49,12 @@ var Cmd = &cli.Command{
|
||||
return err
|
||||
}
|
||||
|
||||
// Write cli token to the repo if not there yet
|
||||
if _, err := api.AuthNew(ctx, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: properly parse api endpoint (or make it a URL)
|
||||
return serveRPC(api, "127.0.0.1:"+cctx.String("api"))
|
||||
return serveRPC(api, "127.0.0.1:"+cctx.String("api"), api.AuthVerify)
|
||||
},
|
||||
}
|
||||
|
@ -1,15 +1,23 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/filecoin-project/go-lotus/lib/auth"
|
||||
"net/http"
|
||||
|
||||
"github.com/filecoin-project/go-lotus/api"
|
||||
"github.com/filecoin-project/go-lotus/lib/jsonrpc"
|
||||
)
|
||||
|
||||
func serveRPC(api api.API, addr string) error {
|
||||
func serveRPC(a api.API, addr string, verify func(ctx context.Context, token string) ([]string, error)) error {
|
||||
rpcServer := jsonrpc.NewServer()
|
||||
rpcServer.Register("Filecoin", api)
|
||||
http.Handle("/rpc/v0", rpcServer)
|
||||
rpcServer.Register("Filecoin", api.Permissioned(a))
|
||||
|
||||
authHandler := &auth.Handler{
|
||||
Verify: verify,
|
||||
Next: rpcServer.ServeHTTP,
|
||||
}
|
||||
|
||||
http.Handle("/rpc/v0", authHandler)
|
||||
return http.ListenAndServe(addr, http.DefaultServeMux)
|
||||
}
|
||||
|
25
docs/API.md
Normal file
25
docs/API.md
Normal file
@ -0,0 +1,25 @@
|
||||
TODO: make this into a nicer doc
|
||||
|
||||
### Endpoints
|
||||
|
||||
By default `127.0.0.1:1234` - daemon stores the api endpoint multiaddr in `~/.lotus/api`
|
||||
|
||||
* `http://[api:port]/rpc/v0` - jsonrpc http endpoint
|
||||
* `ws://[api:port]/rpc/v0` - jsonrpc websocket endpoint
|
||||
|
||||
### Auth:
|
||||
|
||||
JWT in the `Authorization: Bearer <token>` http header
|
||||
|
||||
Permissions:
|
||||
* `read` - Read node state, no private data
|
||||
* `write` - Write to local store / chain, read private data
|
||||
* `sign` - Use private keys stored in wallet for signing
|
||||
* `admin` - Manage permissions
|
||||
|
||||
Payload:
|
||||
```json
|
||||
{
|
||||
"Allow": ["read", "write", ...]
|
||||
}
|
||||
```
|
2
go.mod
2
go.mod
@ -6,6 +6,7 @@ require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/filecoin-project/go-bls-sigs v0.0.0-20190718224239-4bc4b8a7bbf8
|
||||
github.com/filecoin-project/go-leb128 v0.0.0-20190212224330-8d79a5489543
|
||||
github.com/gbrlsnchs/jwt/v3 v3.0.0-beta.1
|
||||
github.com/gorilla/websocket v1.4.0
|
||||
github.com/ipfs/go-bitswap v0.1.5
|
||||
github.com/ipfs/go-block-format v0.0.2
|
||||
@ -67,7 +68,6 @@ require (
|
||||
go4.org v0.0.0-20190313082347-94abd6928b1d // indirect
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
|
||||
google.golang.org/appengine v1.4.0 // indirect
|
||||
gopkg.in/urfave/cli.v2 v2.0.0-20180128182452-d3ae77c26ac8
|
||||
launchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect
|
||||
)
|
||||
|
2
go.sum
2
go.sum
@ -52,6 +52,8 @@ github.com/filecoin-project/go-leb128 v0.0.0-20190212224330-8d79a5489543 h1:aMJG
|
||||
github.com/filecoin-project/go-leb128 v0.0.0-20190212224330-8d79a5489543/go.mod h1:mjrHv1cDGJWDlGmC0eDc1E5VJr8DmL9XMUcaFwiuKg8=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gbrlsnchs/jwt/v3 v3.0.0-beta.1 h1:EzDjxMg43q1tA2c0MV3tNbaontnHLplHyFF6M5KiVP0=
|
||||
github.com/gbrlsnchs/jwt/v3 v3.0.0-beta.1/go.mod h1:0eHX/BVySxPc6SE2mZRoppGq7qcEagxdmQnA3dzork8=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||
|
42
lib/auth/handler.go
Normal file
42
lib/auth/handler.go
Normal file
@ -0,0 +1,42 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/filecoin-project/go-lotus/api"
|
||||
logging "github.com/ipfs/go-log"
|
||||
)
|
||||
|
||||
var log = logging.Logger("auth")
|
||||
|
||||
type Handler struct {
|
||||
Verify func(ctx context.Context, token string) ([]string, error)
|
||||
Next http.HandlerFunc
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
token := r.Header.Get("Authorization")
|
||||
if token != "" {
|
||||
if !strings.HasPrefix(token, "Bearer ") {
|
||||
log.Warn("missing Bearer prefix in auth header")
|
||||
w.WriteHeader(401)
|
||||
return
|
||||
}
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
|
||||
allow, err := h.Verify(ctx, token)
|
||||
if err != nil {
|
||||
log.Warnf("JWT Verification failed: %s", err)
|
||||
w.WriteHeader(401)
|
||||
return
|
||||
}
|
||||
|
||||
ctx = api.WithPerm(ctx, allow)
|
||||
}
|
||||
|
||||
h.Next(w, r.WithContext(ctx))
|
||||
}
|
@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
|
||||
@ -57,7 +58,7 @@ type ClientCloser func()
|
||||
// handler must be pointer to a struct with function fields
|
||||
// Returned value closes the client connection
|
||||
// TODO: Example
|
||||
func NewClient(addr string, namespace string, handler interface{}) (ClientCloser, error) {
|
||||
func NewClient(addr string, namespace string, handler interface{}, requestHeader http.Header) (ClientCloser, error) {
|
||||
htyp := reflect.TypeOf(handler)
|
||||
if htyp.Kind() != reflect.Ptr {
|
||||
return nil, xerrors.New("expected handler to be a pointer")
|
||||
@ -71,7 +72,7 @@ func NewClient(addr string, namespace string, handler interface{}) (ClientCloser
|
||||
|
||||
var idCtr int64
|
||||
|
||||
conn, _, err := websocket.DefaultDialer.Dial(addr, nil)
|
||||
conn, _, err := websocket.DefaultDialer.Dial(addr, requestHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ func TestRPC(t *testing.T) {
|
||||
AddGet func(int) int
|
||||
StringMatch func(t TestType, i2 int64) (out TestOut, err error)
|
||||
}
|
||||
closer, err := NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &client)
|
||||
closer, err := NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &client, nil)
|
||||
require.NoError(t, err)
|
||||
defer closer()
|
||||
|
||||
@ -111,7 +111,7 @@ func TestRPC(t *testing.T) {
|
||||
var noret struct {
|
||||
Add func(int)
|
||||
}
|
||||
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &noret)
|
||||
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &noret, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// this one should actually work
|
||||
@ -122,7 +122,7 @@ func TestRPC(t *testing.T) {
|
||||
var noparam struct {
|
||||
Add func()
|
||||
}
|
||||
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &noparam)
|
||||
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &noparam, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// shouldn't panic
|
||||
@ -132,7 +132,7 @@ func TestRPC(t *testing.T) {
|
||||
var erronly struct {
|
||||
AddGet func() (int, error)
|
||||
}
|
||||
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &erronly)
|
||||
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &erronly, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = erronly.AddGet()
|
||||
@ -144,7 +144,7 @@ func TestRPC(t *testing.T) {
|
||||
var wrongtype struct {
|
||||
Add func(string) error
|
||||
}
|
||||
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &wrongtype)
|
||||
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", &wrongtype, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = wrongtype.Add("not an int")
|
||||
@ -156,7 +156,7 @@ func TestRPC(t *testing.T) {
|
||||
var notfound struct {
|
||||
NotThere func(string) error
|
||||
}
|
||||
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", ¬found)
|
||||
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "SimpleServerHandler", ¬found, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = notfound.NotThere("hello?")
|
||||
@ -203,7 +203,7 @@ func TestCtx(t *testing.T) {
|
||||
var client struct {
|
||||
Test func(ctx context.Context)
|
||||
}
|
||||
closer, err := NewClient("ws://"+testServ.Listener.Addr().String(), "CtxHandler", &client)
|
||||
closer, err := NewClient("ws://"+testServ.Listener.Addr().String(), "CtxHandler", &client, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
@ -224,7 +224,7 @@ func TestCtx(t *testing.T) {
|
||||
var noCtxClient struct {
|
||||
Test func()
|
||||
}
|
||||
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "CtxHandler", &noCtxClient)
|
||||
closer, err = NewClient("ws://"+testServ.Listener.Addr().String(), "CtxHandler", &noCtxClient, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -264,7 +264,7 @@ func TestUnmarshalableResult(t *testing.T) {
|
||||
testServ := httptest.NewServer(rpcServer)
|
||||
defer testServ.Close()
|
||||
|
||||
closer, err := NewClient("ws://"+testServ.Listener.Addr().String(), "Handler", &client)
|
||||
closer, err := NewClient("ws://"+testServ.Listener.Addr().String(), "Handler", &client, nil)
|
||||
require.NoError(t, err)
|
||||
defer closer()
|
||||
|
||||
@ -325,7 +325,7 @@ func TestChan(t *testing.T) {
|
||||
testServ := httptest.NewServer(rpcServer)
|
||||
defer testServ.Close()
|
||||
|
||||
closer, err := NewClient("ws://"+testServ.Listener.Addr().String(), "ChanHandler", &client)
|
||||
closer, err := NewClient("ws://"+testServ.Listener.Addr().String(), "ChanHandler", &client, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer closer()
|
||||
|
@ -1,11 +1,11 @@
|
||||
package jsonrpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/gorilla/websocket"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -28,7 +28,7 @@ func NewServer() *RPCServer {
|
||||
|
||||
var upgrader = websocket.Upgrader{}
|
||||
|
||||
func (s *RPCServer) handleWS(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *RPCServer) handleWS(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
@ -39,7 +39,7 @@ func (s *RPCServer) handleWS(w http.ResponseWriter, r *http.Request) {
|
||||
(&wsConn{
|
||||
conn: c,
|
||||
handler: s.methods,
|
||||
}).handleWsConn(r.Context())
|
||||
}).handleWsConn(ctx)
|
||||
|
||||
if err := c.Close(); err != nil {
|
||||
log.Error(err)
|
||||
@ -49,12 +49,14 @@ func (s *RPCServer) handleWS(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// TODO: return errors to clients per spec
|
||||
func (s *RPCServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
if r.Header.Get("Connection") == "Upgrade" {
|
||||
s.handleWS(w, r)
|
||||
s.handleWS(ctx, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
s.methods.handleReader(r.Context(), r.Body, w, rpcError)
|
||||
s.methods.handleReader(ctx, r.Body, w, rpcError)
|
||||
}
|
||||
|
||||
func rpcError(wf func(func(io.Writer)), req *request, code int, err error) {
|
||||
|
38
node/api.go
38
node/api.go
@ -10,22 +10,50 @@ import (
|
||||
"github.com/filecoin-project/go-lotus/chain/types"
|
||||
"github.com/filecoin-project/go-lotus/miner"
|
||||
"github.com/filecoin-project/go-lotus/node/client"
|
||||
"github.com/filecoin-project/go-lotus/node/modules"
|
||||
|
||||
"github.com/gbrlsnchs/jwt/v3"
|
||||
"github.com/ipfs/go-cid"
|
||||
logging "github.com/ipfs/go-log"
|
||||
"github.com/libp2p/go-libp2p-core/host"
|
||||
"github.com/libp2p/go-libp2p-core/peer"
|
||||
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
||||
ma "github.com/multiformats/go-multiaddr"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
var log = logging.Logger("node")
|
||||
|
||||
type API struct {
|
||||
client.LocalStorage
|
||||
|
||||
Host host.Host
|
||||
Chain *chain.ChainStore
|
||||
PubSub *pubsub.PubSub
|
||||
Mpool *chain.MessagePool
|
||||
Wallet *chain.Wallet
|
||||
Host host.Host
|
||||
Chain *chain.ChainStore
|
||||
PubSub *pubsub.PubSub
|
||||
Mpool *chain.MessagePool
|
||||
Wallet *chain.Wallet
|
||||
APISecret *modules.APIAlg
|
||||
}
|
||||
|
||||
type jwtPayload struct {
|
||||
Allow []string
|
||||
}
|
||||
|
||||
func (a *API) AuthVerify(ctx context.Context, token string) ([]string, error) {
|
||||
var payload jwtPayload
|
||||
if _, err := jwt.Verify([]byte(token), (*jwt.HMACSHA)(a.APISecret), &payload); err != nil {
|
||||
return nil, xerrors.Errorf("JWT Verification failed: %w", err)
|
||||
}
|
||||
|
||||
return payload.Allow, nil
|
||||
}
|
||||
|
||||
func (a *API) AuthNew(ctx context.Context, perms []string) ([]byte, error) {
|
||||
p := jwtPayload{
|
||||
Allow: perms, // TODO: consider checking validity
|
||||
}
|
||||
|
||||
return jwt.Sign(&p, (*jwt.HMACSHA)(a.APISecret))
|
||||
}
|
||||
|
||||
func (a *API) ChainSubmitBlock(ctx context.Context, blk *chain.BlockMsg) error {
|
||||
|
@ -70,6 +70,7 @@ const (
|
||||
HandleIncomingBlocksKey
|
||||
HandleIncomingMessagesKey
|
||||
|
||||
// daemon
|
||||
SetApiEndpointKey
|
||||
|
||||
_nInvokes // keep this last
|
||||
@ -225,6 +226,8 @@ func Repo(r repo.Repo) Option {
|
||||
Override(new(peer.ID), peer.IDFromPublicKey),
|
||||
|
||||
Override(new(types.KeyStore), modules.KeyStore),
|
||||
|
||||
Override(new(*modules.APIAlg), modules.APISecret),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,12 @@ package modules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"github.com/filecoin-project/go-lotus/api"
|
||||
"github.com/gbrlsnchs/jwt/v3"
|
||||
"golang.org/x/xerrors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ipfs/go-bitswap"
|
||||
@ -70,6 +76,51 @@ func KeyStore(lr repo.LockedRepo) (types.KeyStore, error) {
|
||||
return lr.KeyStore()
|
||||
}
|
||||
|
||||
const JWTSecretName = "auth-jwt-private"
|
||||
|
||||
type APIAlg jwt.HMACSHA
|
||||
|
||||
type jwtPayload struct {
|
||||
Allow []string
|
||||
}
|
||||
|
||||
func APISecret(keystore types.KeyStore, lr repo.LockedRepo) (*APIAlg, error) {
|
||||
key, err := keystore.Get(JWTSecretName)
|
||||
if err != nil {
|
||||
log.Warn("Generating new API secret")
|
||||
|
||||
sk, err := ioutil.ReadAll(io.LimitReader(rand.Reader, 32))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key = types.KeyInfo{
|
||||
Type: "jwt-hmac-secret",
|
||||
PrivateKey: sk,
|
||||
}
|
||||
|
||||
if err := keystore.Put(JWTSecretName, key); err != nil {
|
||||
return nil, xerrors.Errorf("writing API secret: %w", err)
|
||||
}
|
||||
|
||||
// TODO: make this configurable
|
||||
p := jwtPayload{
|
||||
Allow: api.AllPermissions,
|
||||
}
|
||||
|
||||
cliToken, err := jwt.Sign(&p, jwt.NewHS256(key.PrivateKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := lr.SetAPIToken(cliToken); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return (*APIAlg)(jwt.NewHS256(key.PrivateKey)), nil
|
||||
}
|
||||
|
||||
func Datastore(r repo.LockedRepo) (datastore.Batching, error) {
|
||||
return r.Datastore("/metadata")
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ func rpcBuilder(t *testing.T, n int) []api.API {
|
||||
testServ := httptest.NewServer(rpcServer) // todo: close
|
||||
|
||||
var err error
|
||||
out[i], err = client.NewRPC("ws://" + testServ.Listener.Addr().String())
|
||||
out[i], err = client.NewRPC("ws://"+testServ.Listener.Addr().String(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import (
|
||||
|
||||
const (
|
||||
fsAPI = "api"
|
||||
fsAPIToken = "token"
|
||||
fsConfig = "config.toml"
|
||||
fsDatastore = "datastore"
|
||||
fsLibp2pKey = "libp2p.priv"
|
||||
@ -109,6 +110,20 @@ func (fsr *FsRepo) APIEndpoint() (multiaddr.Multiaddr, error) {
|
||||
return apima, nil
|
||||
}
|
||||
|
||||
func (fsr *FsRepo) APIToken() ([]byte, error) {
|
||||
p := filepath.Join(fsr.path, fsAPIToken)
|
||||
f, err := os.Open(p)
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return nil, ErrNoAPIEndpoint
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close() //nolint: errcheck // Read only op
|
||||
|
||||
return ioutil.ReadAll(f)
|
||||
}
|
||||
|
||||
// Lock acquires exclusive lock on this repo
|
||||
func (fsr *FsRepo) Lock() (LockedRepo, error) {
|
||||
locked, err := fslock.Locked(fsr.path, fsLock)
|
||||
@ -245,6 +260,13 @@ func (fsr *fsLockedRepo) SetAPIEndpoint(ma multiaddr.Multiaddr) error {
|
||||
return ioutil.WriteFile(fsr.join(fsAPI), []byte(ma.String()), 0644)
|
||||
}
|
||||
|
||||
func (fsr *fsLockedRepo) SetAPIToken(token []byte) error {
|
||||
if err := fsr.stillValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(fsr.join(fsAPIToken), token, 0600)
|
||||
}
|
||||
|
||||
func (fsr *fsLockedRepo) KeyStore() (types.KeyStore, error) {
|
||||
if err := fsr.stillValid(); err != nil {
|
||||
return nil, err
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
|
||||
var (
|
||||
ErrNoAPIEndpoint = errors.New("API not running (no endpoint)")
|
||||
ErrNoAPIToken = errors.New("API token not set")
|
||||
ErrRepoAlreadyLocked = errors.New("repo is already locked")
|
||||
ErrClosedRepo = errors.New("repo is no longer open")
|
||||
|
||||
@ -24,6 +25,9 @@ type Repo interface {
|
||||
// APIEndpoint returns multiaddress for communication with Lotus API
|
||||
APIEndpoint() (multiaddr.Multiaddr, error)
|
||||
|
||||
// APIToken returns JWT API Token for use in operations that require auth
|
||||
APIToken() ([]byte, error)
|
||||
|
||||
// Lock locks the repo for exclusive use.
|
||||
Lock() (LockedRepo, error)
|
||||
}
|
||||
@ -45,6 +49,9 @@ type LockedRepo interface {
|
||||
// so it can be read by API clients
|
||||
SetAPIEndpoint(multiaddr.Multiaddr) error
|
||||
|
||||
// SetAPIToken sets JWT API Token for CLI
|
||||
SetAPIToken([]byte) error
|
||||
|
||||
// KeyStore returns store of private keys for Filecoin transactions
|
||||
KeyStore() (types.KeyStore, error)
|
||||
|
||||
|
@ -18,7 +18,8 @@ import (
|
||||
type MemRepo struct {
|
||||
api struct {
|
||||
sync.Mutex
|
||||
ma multiaddr.Multiaddr
|
||||
ma multiaddr.Multiaddr
|
||||
token []byte
|
||||
}
|
||||
|
||||
repoLock chan struct{}
|
||||
@ -102,6 +103,15 @@ func (mem *MemRepo) APIEndpoint() (multiaddr.Multiaddr, error) {
|
||||
return mem.api.ma, nil
|
||||
}
|
||||
|
||||
func (mem *MemRepo) APIToken() ([]byte, error) {
|
||||
mem.api.Lock()
|
||||
defer mem.api.Unlock()
|
||||
if mem.api.ma == nil {
|
||||
return nil, ErrNoAPIToken
|
||||
}
|
||||
return mem.api.token, nil
|
||||
}
|
||||
|
||||
func (mem *MemRepo) Lock() (LockedRepo, error) {
|
||||
select {
|
||||
case mem.repoLock <- struct{}{}:
|
||||
@ -177,6 +187,16 @@ func (lmem *lockedMemRepo) SetAPIEndpoint(ma multiaddr.Multiaddr) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lmem *lockedMemRepo) SetAPIToken(token []byte) error {
|
||||
if err := lmem.checkToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
lmem.mem.api.Lock()
|
||||
lmem.mem.api.token = token
|
||||
lmem.mem.api.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lmem *lockedMemRepo) KeyStore() (types.KeyStore, error) {
|
||||
if err := lmem.checkToken(); err != nil {
|
||||
return nil, err
|
||||
|
Loading…
Reference in New Issue
Block a user