From 51fc0fca60439d00888e61956601934d9214b69b Mon Sep 17 00:00:00 2001 From: Ralf Schmitt Date: Thu, 31 Mar 2022 11:19:40 +0200 Subject: [PATCH] Implement json rpc proxy --- rolling-shutter/.golangci.yml | 2 +- rolling-shutter/cmd/proxy/proxy.go | 81 ++++++++++++++++++++++ rolling-shutter/cmd/root.go | 2 + rolling-shutter/go.mod | 2 +- rolling-shutter/medley/decodehooks.go | 8 +++ rolling-shutter/proxy/proxy.go | 98 +++++++++++++++++++++++++++ 6 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 rolling-shutter/cmd/proxy/proxy.go create mode 100644 rolling-shutter/proxy/proxy.go diff --git a/rolling-shutter/.golangci.yml b/rolling-shutter/.golangci.yml index 4ba79452..13ca81ac 100644 --- a/rolling-shutter/.golangci.yml +++ b/rolling-shutter/.golangci.yml @@ -161,7 +161,7 @@ linters: issues: exclude: - "typeUnparen: could simplify \\(func.* to func\\(" - - "Error return value of `.*Mark.*FlagRequired` is not checked" + - "Error return value of `.*Mark.*Flag.*` is not checked" - "Error return value of `viper.BindEnv` is not checked" - 'shadow: declaration of "err" shadows declaration at line' - "Expect WriteFile permissions to be" diff --git a/rolling-shutter/cmd/proxy/proxy.go b/rolling-shutter/cmd/proxy/proxy.go new file mode 100644 index 00000000..3bc4cad8 --- /dev/null +++ b/rolling-shutter/cmd/proxy/proxy.go @@ -0,0 +1,81 @@ +package proxy + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/shutter-network/shutter/shuttermint/cmd/shversion" + "github.com/shutter-network/shutter/shuttermint/proxy" +) + +var cfgFile string + +func Cmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "proxy", + Short: "Run a json rpc proxy", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return proxyMain() + }, + } + cmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file") + cmd.MarkPersistentFlagRequired("config") + cmd.MarkPersistentFlagFilename("config") + return cmd +} + +func readConfig() (proxy.Config, error) { + config := proxy.Config{} + viper.AddConfigPath("$HOME/.config/shutter") + viper.SetConfigName("proxy") + viper.SetConfigType("toml") + viper.SetConfigFile(cfgFile) + var err error + err = viper.ReadInConfig() + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + // Config file not found + if cfgFile != "" { + return config, err + } + } else if err != nil { + return config, err // Config file was found but another error was produced + } + err = config.Unmarshal(viper.GetViper()) + return config, err +} + +func proxyMain() error { + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + + config, err := readConfig() + if err != nil { + return err + } + + log.Info().Msgf("Starting shutter proxy version %s", shversion.Version()) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + termChan := make(chan os.Signal, 1) + signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM) + go func() { + sig := <-termChan + log.Info().Str("signal", sig.String()).Msg("Received signal, shutting down") + cancel() + }() + + err = proxy.Run(ctx, config) + if err == context.Canceled { + log.Info().Msg("Bye.") + return nil + } + return err +} diff --git a/rolling-shutter/cmd/root.go b/rolling-shutter/cmd/root.go index 9bbbdb60..99e4a31e 100644 --- a/rolling-shutter/cmd/root.go +++ b/rolling-shutter/cmd/root.go @@ -14,6 +14,7 @@ import ( "github.com/shutter-network/shutter/shuttermint/cmd/cryptocmd" "github.com/shutter-network/shutter/shuttermint/cmd/keyper" "github.com/shutter-network/shutter/shuttermint/cmd/mocknode" + "github.com/shutter-network/shutter/shuttermint/cmd/proxy" "github.com/shutter-network/shutter/shuttermint/cmd/shversion" "github.com/shutter-network/shutter/shuttermint/cmd/snapshot" "github.com/shutter-network/shutter/shuttermint/medley" @@ -67,5 +68,6 @@ func Cmd() *cobra.Command { cmd.AddCommand(mocknode.Cmd()) cmd.AddCommand(snapshot.Cmd()) cmd.AddCommand(cryptocmd.Cmd()) + cmd.AddCommand(proxy.Cmd()) return cmd } diff --git a/rolling-shutter/go.mod b/rolling-shutter/go.mod index a705172f..e0120e15 100644 --- a/rolling-shutter/go.mod +++ b/rolling-shutter/go.mod @@ -19,6 +19,7 @@ require ( github.com/multiformats/go-multiaddr v0.3.3 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.12.1 + github.com/rs/zerolog v1.26.1 github.com/shutter-network/shutter/shlib v0.1.9 github.com/spf13/cobra v1.4.0 github.com/spf13/pflag v1.0.5 @@ -180,7 +181,6 @@ require ( github.com/rjeczalik/notify v0.9.2 // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/rs/cors v1.8.2 // indirect - github.com/rs/zerolog v1.26.1 // indirect github.com/sasha-s/go-deadlock v0.2.1-0.20190427202633-1595213edefa // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 // indirect diff --git a/rolling-shutter/medley/decodehooks.go b/rolling-shutter/medley/decodehooks.go index b6b25258..250fef80 100644 --- a/rolling-shutter/medley/decodehooks.go +++ b/rolling-shutter/medley/decodehooks.go @@ -5,6 +5,7 @@ import ( "crypto/ed25519" "encoding/hex" "fmt" + "net/url" "reflect" "github.com/ethereum/go-ethereum/common" @@ -46,6 +47,13 @@ func P2PKeyHook(f reflect.Type, t reflect.Type, data interface{}) (interface{}, return privkey, nil } +func StringToURL(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { + if f.Kind() != reflect.String || t != reflect.TypeOf(&url.URL{}) { + return data, nil + } + return url.Parse(data.(string)) +} + func StringToEd25519PrivateKey(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { if f.Kind() != reflect.String || t != reflect.TypeOf(ed25519.PrivateKey{}) { return data, nil diff --git a/rolling-shutter/proxy/proxy.go b/rolling-shutter/proxy/proxy.go new file mode 100644 index 00000000..d6934890 --- /dev/null +++ b/rolling-shutter/proxy/proxy.go @@ -0,0 +1,98 @@ +// Package proxy contains a jsonrpc proxy implementation. +package proxy + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/http/httputil" + "net/url" + "time" + + "github.com/go-chi/chi/v5" + "github.com/mitchellh/mapstructure" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" + "golang.org/x/sync/errgroup" + + "github.com/shutter-network/shutter/shuttermint/medley" +) + +type RPCRequest struct { + Version string `json:"jsonrpc"` + Method string `json:"method,omitempty"` + Params interface{} `json:"params,omitempty"` + ID interface{} `json:"id,omitempty"` +} + +type Config struct { + CollatorURL, SequencerURL *url.URL + HTTPListenAddress string +} + +func (config *Config) Unmarshal(v *viper.Viper) error { + err := v.Unmarshal(config, viper.DecodeHook( + mapstructure.ComposeDecodeHookFunc( + medley.StringToURL, + ))) + return err +} + +type JSONRPCProxy struct { + collator, sequencer *httputil.ReverseProxy +} + +func (p *JSONRPCProxy) SelectReverseProxy(method string) *httputil.ReverseProxy { + switch method { + case "eth_sendTransaction": + return p.collator + case "eth_sendRawTransaction": + return p.collator + default: + return p.sequencer + } +} + +func (p *JSONRPCProxy) HandleRequest(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Bad request", http.StatusBadRequest) + return + } + rpcreq := RPCRequest{} + err = json.Unmarshal(body, &rpcreq) + if err != nil { + http.Error(w, "Bad request", http.StatusBadRequest) + return + } + log.Info().Str("method", rpcreq.Method).Msg("dispatching") + + // make the body available again before letting reverse proxy handle the rest + r.Body = io.NopCloser(bytes.NewBuffer(body)) + p.SelectReverseProxy(rpcreq.Method).ServeHTTP(w, r) +} + +func Run(ctx context.Context, config Config) error { + p := JSONRPCProxy{ + collator: httputil.NewSingleHostReverseProxy(config.CollatorURL), + sequencer: httputil.NewSingleHostReverseProxy(config.SequencerURL), + } + router := chi.NewRouter() + router.Post("/*", p.HandleRequest) + + httpServer := &http.Server{ + Addr: config.HTTPListenAddress, + Handler: router, + } + errorgroup, errorctx := errgroup.WithContext(ctx) + errorgroup.Go(httpServer.ListenAndServe) + errorgroup.Go(func() error { + <-errorctx.Done() + shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + return httpServer.Shutdown(shutdownCtx) + }) + return errorgroup.Wait() +}