From d4ab79ece0c04a24eb987dfce4b251be35be3b12 Mon Sep 17 00:00:00 2001 From: Emmanuel Odeke Date: Fri, 28 Jul 2017 14:13:12 -0600 Subject: [PATCH] client/rest, cmd/baseserver: started a basecoin REST client ```shell $ go get -u -v github.com/tendermint/basecoin/cmd/baseserver $ baseserver init $ baseserver serve ``` A server that can be ran by default on port 8998 otherwise one can specify the port using flag `--port` like this: ```shell $ baseserver serve --port 9999 ``` to serve it on port 9999, accessible at http://localhost:9999 Implemented: - [X] /keys POST -- generate a new key - [X] /keys GET -- list all keys - [X] /keys/{name} DELETE-- delete a named key - [X] /keys/{name} GET -- get a named key - [X] /keys/{name} POST, PUT -- update a named key - [X] /sign POST -- sign a transaction - [X] /build/send POST -- send money from one actor to another. However, still needs testing and verification of output - [X] /tx POST -- post a transaction to the blockchain. However, still needs testing and verification of output This base code to get the handlers starters was adapted from: * https://github.com/tendermint/go-crypto/blob/master/keys/server * https://github.com/tendermint/basecoin/blob/unstable/client/commands/proxy/root.go Updates #186 --- client/rest/README.md | 25 +++++ client/rest/handlers.go | 234 +++++++++++++++++++++++++++++++++++++++ client/rest/proxy.go | 35 ++++++ client/rest/types.go | 114 +++++++++++++++++++ cmd/baseserver/README.md | 116 +++++++++++++++++++ cmd/baseserver/main.go | 66 +++++++++++ 6 files changed, 590 insertions(+) create mode 100644 client/rest/README.md create mode 100644 client/rest/handlers.go create mode 100644 client/rest/proxy.go create mode 100644 client/rest/types.go create mode 100644 cmd/baseserver/README.md create mode 100644 cmd/baseserver/main.go diff --git a/client/rest/README.md b/client/rest/README.md new file mode 100644 index 000000000..8b8e68f22 --- /dev/null +++ b/client/rest/README.md @@ -0,0 +1,25 @@ +## basecoin-server + +### Proxy server +This package exposes access to key management i.e +- creating +- listing +- updating +- deleting + +The HTTP handlers can be embedded in a larger server that +does things like signing transactions and posting them to a +Tendermint chain (which requires domain-knowledge of the transaction +types and is out of scope of this generic app). + +### Key Management +We expose a couple of methods for safely managing your keychain. +If you are embedding this in a larger server, you will typically +want to mount all these paths /keys. + +HTTP Method | Route | Description +---|---|--- +POST|/|Requires a name and passphrase to create a brand new key +GET|/|Retrieves the list of all available key names, along with their public key and address +GET|/{name} | Updates the passphrase for the given key. It requires you to correctly provide the current passphrase, as well as a new one. +DELETE|/{name} | Permanently delete this private key. It requires you to correctly provide the current passphrase. diff --git a/client/rest/handlers.go b/client/rest/handlers.go new file mode 100644 index 000000000..70b9c57bb --- /dev/null +++ b/client/rest/handlers.go @@ -0,0 +1,234 @@ +package rest + +import ( + "net/http" + "strings" + + "github.com/gorilla/mux" + "github.com/pkg/errors" + + "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/client/commands" + "github.com/tendermint/basecoin/modules/auth" + "github.com/tendermint/basecoin/modules/base" + "github.com/tendermint/basecoin/modules/coin" + "github.com/tendermint/basecoin/modules/fee" + "github.com/tendermint/basecoin/modules/nonce" + keysutils "github.com/tendermint/go-crypto/cmd" + keys "github.com/tendermint/go-crypto/keys" +) + +type Keys struct { + algo string + manager keys.Manager +} + +func DefaultKeysManager() keys.Manager { + return keysutils.GetKeyManager() +} + +func New(manager keys.Manager, algo string) *Keys { + return &Keys{ + algo: algo, + manager: manager, + } +} + +func (k *Keys) GenerateKey(w http.ResponseWriter, r *http.Request) { + ckReq := &CreateKeyRequest{ + Algo: k.algo, + } + if err := parseRequestJSON(r, ckReq); err != nil { + writeError(w, err) + return + } + + key, seed, err := k.manager.Create(ckReq.Name, ckReq.Passphrase, ckReq.Algo) + if err != nil { + writeError(w, err) + return + } + + res := &CreateKeyResponse{Key: key, Seed: seed} + writeSuccess(w, res) +} + +func (k *Keys) GetKey(w http.ResponseWriter, r *http.Request) { + query := mux.Vars(r) + name := query["name"] + key, err := k.manager.Get(name) + if err != nil { + writeError(w, err) + return + } + writeSuccess(w, &key) +} + +func (k *Keys) ListKeys(w http.ResponseWriter, r *http.Request) { + keys, err := k.manager.List() + if err != nil { + writeError(w, err) + return + } + writeSuccess(w, keys) +} + +var ( + errNonMatchingPathAndJSONKeyNames = errors.New("path and json key names don't match") +) + +func (k *Keys) UpdateKey(w http.ResponseWriter, r *http.Request) { + uReq := new(UpdateKeyRequest) + if err := parseRequestJSON(r, uReq); err != nil { + writeError(w, err) + return + } + + query := mux.Vars(r) + name := query["name"] + if name != uReq.Name { + writeError(w, errNonMatchingPathAndJSONKeyNames) + return + } + + if err := k.manager.Update(uReq.Name, uReq.OldPass, uReq.NewPass); err != nil { + writeError(w, err) + return + } + + key, err := k.manager.Get(uReq.Name) + if err != nil { + writeError(w, err) + return + } + writeSuccess(w, &key) +} + +func (k *Keys) DeleteKey(w http.ResponseWriter, r *http.Request) { + dReq := new(DeleteKeyRequest) + if err := parseRequestJSON(r, dReq); err != nil { + writeError(w, err) + return + } + + query := mux.Vars(r) + name := query["name"] + if name != dReq.Name { + writeError(w, errNonMatchingPathAndJSONKeyNames) + return + } + + if err := k.manager.Delete(dReq.Name, dReq.Passphrase); err != nil { + writeError(w, err) + return + } + + resp := &ErrorResponse{Success: true} + writeSuccess(w, resp) +} + +func (k *Keys) Register(r *mux.Router) { + r.HandleFunc("/keys", k.GenerateKey).Methods("POST") + r.HandleFunc("/keys", k.ListKeys).Methods("GET") + r.HandleFunc("/keys/{name}", k.GetKey).Methods("GET") + r.HandleFunc("/keys/{name}", k.UpdateKey).Methods("POST", "PUT") + r.HandleFunc("/keys/{name}", k.DeleteKey).Methods("DELETE") +} + +type Context struct { + Keys *Keys +} + +func (ctx *Context) RegisterHandlers(r *mux.Router) error { + ctx.Keys.Register(r) + r.HandleFunc("/build/send", doSend).Methods("POST") + r.HandleFunc("/sign", doSign).Methods("POST") + r.HandleFunc("/tx", doPostTx).Methods("POST") + + return nil +} + +func doPostTx(w http.ResponseWriter, r *http.Request) { + tx := new(basecoin.Tx) + if err := parseRequestJSON(r, tx); err != nil { + writeError(w, err) + return + } + commit, err := PostTx(*tx) + if err != nil { + writeError(w, err) + return + } + + writeSuccess(w, commit) +} + +func doSign(w http.ResponseWriter, r *http.Request) { + sr := new(SignRequest) + if err := parseRequestJSON(r, sr); err != nil { + writeError(w, err) + return + } + + tx := sr.Tx + if err := SignTx(sr.Name, sr.Password, tx); err != nil { + writeError(w, err) + return + } + writeSuccess(w, tx) +} + +func doSend(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + si := new(SendInput) + if err := parseRequestJSON(r, si); err != nil { + writeError(w, err) + return + } + + var errsList []string + if si.From == nil { + errsList = append(errsList, `"from" cannot be nil`) + } + if si.Sequence <= 0 { + errsList = append(errsList, `"sequence" must be > 0`) + } + if si.To == nil { + errsList = append(errsList, `"to" cannot be nil`) + } + if si.Fees == nil { + errsList = append(errsList, `"fees" cannot be nil`) + } + if len(errsList) > 0 { + err := &ErrorResponse{ + Error: strings.Join(errsList, ", "), + Code: 406, + } + writeCode(w, err, 406) + return + } + + coins := []coin.Coin{*si.Fees} + in := []coin.TxInput{ + coin.NewTxInput(*si.From, coins), + } + out := []coin.TxOutput{ + coin.NewTxOutput(*si.To, coins), + } + + tx := coin.NewSendTx(in, out) + tx = fee.NewFee(tx, *si.Fees, *si.From) + + signers := []basecoin.Actor{ + *si.From, + *si.To, + } + tx = nonce.NewTx(si.Sequence, signers, tx) + tx = base.NewChainTx(commands.GetChainID(), 0, tx) + + if si.Multi { + tx = auth.NewMulti(tx).Wrap() + } else { + tx = auth.NewSig(tx).Wrap() + } +} diff --git a/client/rest/proxy.go b/client/rest/proxy.go new file mode 100644 index 000000000..d1b721018 --- /dev/null +++ b/client/rest/proxy.go @@ -0,0 +1,35 @@ +package rest + +import ( + "github.com/tendermint/tendermint/rpc/client" + "github.com/tendermint/tendermint/rpc/core" + rpc "github.com/tendermint/tendermint/rpc/lib/server" +) + +func Routes(c client.Client) map[string]*rpc.RPCFunc { + return map[string]*rpc.RPCFunc{ + // subscribe/unsubscribe are reserved for websocket events. + // We can just the core Tendermint implementation, which uses + // the EventSwitch that we registered in NewWebsocketManager above. + "subscribe": rpc.NewWSRPCFunc(core.Subscribe, "event"), + "unsubscribe": rpc.NewWSRPCFunc(core.Unsubscribe, "event"), + + // info API + "status": rpc.NewRPCFunc(c.Status, ""), + "blockchain": rpc.NewRPCFunc(c.BlockchainInfo, "minHeight,maxHeight"), + "genesis": rpc.NewRPCFunc(c.Genesis, ""), + "block": rpc.NewRPCFunc(c.Block, "height"), + "commit": rpc.NewRPCFunc(c.Commit, "height"), + "tx": rpc.NewRPCFunc(c.Tx, "hash.prove"), + "validators": rpc.NewRPCFunc(c.Validators, ""), + + // broadcast API + "broadcast_tx_commit": rpc.NewRPCFunc(c.BroadcastTxCommit, "tx"), + "broadcast_tx_sync": rpc.NewRPCFunc(c.BroadcastTxSync, "tx"), + "broadcast_tx_async": rpc.NewRPCFunc(c.BroadcastTxAsync, "tx"), + + // abci API + "abci_query": rpc.NewRPCFunc(c.ABCIQuery, "path,data,prove"), + "abci_info": rpc.NewRPCFunc(c.ABCIInfo, ""), + } +} diff --git a/client/rest/types.go b/client/rest/types.go new file mode 100644 index 000000000..43af2842b --- /dev/null +++ b/client/rest/types.go @@ -0,0 +1,114 @@ +package rest + +import ( + "encoding/json" + "io/ioutil" + "net/http" + + "github.com/pkg/errors" + "gopkg.in/go-playground/validator.v9" + + "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/modules/coin" + "github.com/tendermint/go-crypto/keys" + data "github.com/tendermint/go-wire/data" +) + +type CreateKeyRequest struct { + Name string `json:"name,omitempty" validate:"required,min=4,printascii"` + Passphrase string `json:"passphrase,omitempty" validate:"required,min=10"` + + // Algo is the requested algorithm to create the key + Algo string `json:"algo,omitempty"` +} + +type DeleteKeyRequest struct { + Name string `json:"name,omitempty" validate:"required,min=4,printascii"` + Passphrase string `json:"passphrase,omitempty" validate:"required,min=10"` +} + +type UpdateKeyRequest struct { + Name string `json:"name,omitempty" validate:"required,min=4,printascii"` + OldPass string `json:"passphrase,omitempty" validate:"required,min=10"` + NewPass string `json:"new_passphrase,omitempty" validate:"required,min=10"` +} + +type SignRequest struct { + Name string `json:"name,omitempty" validate:"required,min=4,printascii"` + Password string `json:"password,omitempty" validate:"required,min=10"` + + Tx basecoin.Tx `json:"tx" validate:"required"` +} + +type ErrorResponse struct { + Success bool `json:"success,omitempty"` + + // Error is the error message if Success is false + Error string `json:"error,omitempty"` + + // Code is set if Success is false + Code int `json:"code,omitempty"` +} + +type CreateKeyResponse struct { + Key keys.Info `json:"key,omitempty"` + Seed string `json:"seed_phrase,omitempty"` +} + +// SendInput is the request to send an amount from one actor to another. +// Note: Not using the `validator:""` tags here because SendInput has +// many fields so it would be nice to figure out all the invalid +// inputs and report them back to the caller, in one shot. +type SendInput struct { + Fees *coin.Coin `json:"amount"` + Multi bool `json:"multi,omitempty"` + Sequence uint32 `json:"sequence"` + + To *basecoin.Actor `json:"to"` + From *basecoin.Actor `json:"from"` +} + +// Validators + +var theValidator = validator.New() + +func validate(req interface{}) error { + return errors.Wrap(theValidator.Struct(req), "Validate") +} + +// Helpers +func parseRequestJSON(r *http.Request, save interface{}) error { + defer r.Body.Close() + + slurp, err := ioutil.ReadAll(r.Body) + if err != nil { + return errors.Wrap(err, "Read Request") + } + if err := json.Unmarshal(slurp, save); err != nil { + return errors.Wrap(err, "Parse") + } + return validate(save) +} + +func writeSuccess(w http.ResponseWriter, data interface{}) { + writeCode(w, data, 200) +} + +func writeCode(w http.ResponseWriter, out interface{}, code int) { + blob, err := data.ToJSON(out) + if err != nil { + writeError(w, err) + } else { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + w.Write(blob) + } +} + +func writeError(w http.ResponseWriter, err error) { + resp := &ErrorResponse{ + Code: 406, + Error: err.Error(), + } + writeCode(w, resp, 406) +} diff --git a/cmd/baseserver/README.md b/cmd/baseserver/README.md new file mode 100644 index 000000000..3b7c9acec --- /dev/null +++ b/cmd/baseserver/README.md @@ -0,0 +1,116 @@ +# baseserver + +baseserver is the REST counterpart to basecli + +## Compiling and running it +```shell +$ go get -u -v github.com/tendermint/basecoin/cmd/baseserver +$ baseserver init +$ baseserver serve --port 8888 +``` + +to run the server at localhost:8888, otherwise if you don't specify --port, +by default the server will be run on port 8998. + +## Supported routes +Route | Method | Completed | Description +---|---|---|--- +/keys|GET|✔️|Lists all keys +/keys|POST|✔️|Generate a new key. It expects fields: "name", "algo", "passphrase" +/keys/{name}|GET|✔️|Retrieves the specific key +/keys/{name}|POST/PUT|✔️|Updates the named key +/keys/{name}|DELETE|✔️|Deletes the named key +/build/send|POST|✔️|Send a transaction +/sign|POST|✔️|Sign a transaction +/tx|POST|✖️|Post a transaction to the blockchain +/seeds/status|GET|✖️|Returns the information on the last seed + +## Sample usage +- Generate a key +```shell +$ curl -X POST http://localhost8998/keys --data '{"algo": "ed25519", "name": "SampleX", "passphrase": "Say no more"}' +``` + +```json +{ + "key": { + "name": "SampleX", + "address": "603EE63C41E322FC7A247864A9CD0181282EB458", + "pubkey": { + "type": "ed25519", + "data": "C050948CFC087F5E1068C7E244DDC30E03702621CC9442A28E6C9EDA7771AA0C" + } + }, + "seed_phrase": "border almost future parade speak soccer bulk orange real brisk caution body river chapter" +} +``` + +- Sign a key +```shell +$ curl -X POST http://localhost:8998/sign --data '{ + "name": "matt", + "password": "Say no more", + "tx": { + "type": "sigs/multi", + "data": { + "tx": {"type":"coin/send","data":{"inputs":[{"address":{"chain":"","app":"role","addr":"62616E6B32"},"coins":[{"denom":"mycoin","amount":900000}]}],"outputs":[{"address":{"chain":"","app":"sigs","addr":"BDADF167E6CF2CDF2D621E590FF1FED2787A40E0"},"coins":[{"denom":"mycoin","amount":900000}]}]}}, + "signatures": null + } + } +}' +``` + +```json +{ + "type": "sigs/multi", + "data": { + "tx": { + "type": "coin/send", + "data": { + "inputs": [ + { + "address": { + "chain": "", + "app": "role", + "addr": "62616E6B32" + }, + "coins": [ + { + "denom": "mycoin", + "amount": 900000 + } + ] + } + ], + "outputs": [ + { + "address": { + "chain": "", + "app": "sigs", + "addr": "BDADF167E6CF2CDF2D621E590FF1FED2787A40E0" + }, + "coins": [ + { + "denom": "mycoin", + "amount": 900000 + } + ] + } + ] + } + }, + "signatures": [ + { + "Sig": { + "type": "ed25519", + "data": "F6FE3053F1E6C236F886A0D525C1AF840F7831B6E50F7E1108C345AA524303920F09945DA110AD5184B3F45717D7114E368B12AFE027FECECC2FC193D4906A0C" + }, + "Pubkey": { + "type": "ed25519", + "data": "0D8D19E527BAE9D1256A3D03009E2708171CDCB71CCDEDA2DC52DD9AD23AEE25" + } + } + ] + } +} +``` diff --git a/cmd/baseserver/main.go b/cmd/baseserver/main.go new file mode 100644 index 000000000..7e88bdfde --- /dev/null +++ b/cmd/baseserver/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + + "github.com/tendermint/basecoin/client/commands" + rest "github.com/tendermint/basecoin/client/rest" + "github.com/tendermint/tmlibs/cli" +) + +var srvCli = &cobra.Command{ + Use: "baseserver", + Short: "Light REST client for tendermint", + Long: `Baseserver presents a nice (not raw hex) interface to the basecoin blockchain structure.`, +} + +var serveCmd = &cobra.Command{ + Use: "serve", + Short: "Serve the light REST client for tendermint", + Long: "Access basecoin via REST", + RunE: serve, +} + +var port int + +func main() { + commands.AddBasicFlags(srvCli) + + flagset := serveCmd.Flags() + flagset.IntVar(&port, "port", 8998, "the port to run the server on") + + srvCli.AddCommand( + commands.InitCmd, + serveCmd, + ) + + // TODO: Decide whether to use $HOME/.basecli for compatibility + // or just use $HOME/.baseserver? + cmd := cli.PrepareMainCmd(srvCli, "BC", os.ExpandEnv("$HOME/.basecli")) + if err := cmd.Execute(); err != nil { + log.Fatal(err) + } +} + +const defaultAlgo = "ed25519" + +func serve(cmd *cobra.Command, args []string) error { + keysManager := rest.DefaultKeysManager() + router := mux.NewRouter() + ctx := rest.Context{ + Keys: rest.New(keysManager, defaultAlgo), + } + if err := ctx.RegisterHandlers(router); err != nil { + return err + } + + addr := fmt.Sprintf(":%d", port) + log.Printf("Serving on %q", addr) + return http.ListenAndServe(addr, router) +}