From d8d7d8d5df981609f4776febdff0d033927250c9 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 31 Oct 2023 04:14:20 -0500 Subject: [PATCH] Enable h2c for http handler. add https handler for http2 (#8610) new flag examples. --https.enabled --https.addr="0.0.0.0" --https.port=443 --https.url="unix:///file.wow" --https.cert="keyfile.cert" --https.key="certfile.cert" also adds support for h2c to the http handler - http2 protocol without tls. --- cmd/rpcdaemon/README.md | 16 +++++- cmd/rpcdaemon/cli/config.go | 81 ++++++++++++++++++++------- cmd/rpcdaemon/cli/httpcfg/http_cfg.go | 7 +++ cmd/utils/flags.go | 7 ++- node/endpoints.go | 74 +++++++++++++++++++++++- turbo/cli/default_flags.go | 1 + turbo/cli/flags.go | 8 ++- 7 files changed, 165 insertions(+), 29 deletions(-) diff --git a/cmd/rpcdaemon/README.md b/cmd/rpcdaemon/README.md index 5ee769e25..7a275d2ac 100644 --- a/cmd/rpcdaemon/README.md +++ b/cmd/rpcdaemon/README.md @@ -194,6 +194,20 @@ If the `--http.url` flag is set, then `--http.addr` and `--http.port` with both note that this is NOT geth-style IPC. for that, read the next section, IPC endpoint(geth-compatible) + +### HTTPS, HTTP2, and H2C + +Erigon supports HTTPS, HTTP2, and H2C out of the box. H2C is served by the default HTTP handler. + +To enable the HTTPS+HTTP2 server, add flag `--https.enabled`, along with providing flags `-https.cert="/path/to.cert"` and `--https.key=/path/to.key` + +By default, the HTTPS server will run on the HTTP port + 363. use flag `--https.port` to set the port + +The HTTPS server will inherit all other configuration parameters from http, for instance, enabling the websocket server, cors domains, or enabled namespaces + +If the `--https.url` flag is set, then `--https.addr` and `--https.port` with both be ignored. + + ### IPC endpoint (geth compatible) erigon supports the geth-style unix socket IPC. you can enable this with `--socket.enabled` flag, @@ -266,7 +280,7 @@ The following table shows the current implementation status of Erigon's RPC daem | eth_getFilterChanges | Yes | | | eth_uninstallFilter | Yes | | | eth_getLogs | Yes | | -| | | | +| interned spe | | | | eth_accounts | No | deprecated | | eth_sendRawTransaction | Yes | `remote`. | | eth_sendTransaction | - | not yet implemented | diff --git a/cmd/rpcdaemon/cli/config.go b/cmd/rpcdaemon/cli/config.go index ea15834ad..614287a63 100644 --- a/cmd/rpcdaemon/cli/config.go +++ b/cmd/rpcdaemon/cli/config.go @@ -89,10 +89,6 @@ func RootCommand() (*cobra.Command, *httpcfg.HttpCfg) { rootCmd.PersistentFlags().StringVar(&cfg.PrivateApiAddr, "private.api.addr", "127.0.0.1:9090", "Erigon's components (txpool, rpcdaemon, sentry, downloader, ...) can be deployed as independent Processes on same/another server. Then components will connect to erigon by this internal grpc API. Example: 127.0.0.1:9090") rootCmd.PersistentFlags().StringVar(&cfg.DataDir, "datadir", "", "path to Erigon working directory") rootCmd.PersistentFlags().BoolVar(&cfg.GraphQLEnabled, "graphql", false, "enables graphql endpoint (disabled by default)") - rootCmd.PersistentFlags().StringVar(&cfg.HttpListenAddress, "http.addr", nodecfg.DefaultHTTPHost, "HTTP-RPC server listening interface") - rootCmd.PersistentFlags().StringVar(&cfg.TLSCertfile, "tls.cert", "", "certificate for client side TLS handshake") - rootCmd.PersistentFlags().StringVar(&cfg.TLSKeyFile, "tls.key", "", "key file for client side TLS handshake") - rootCmd.PersistentFlags().StringVar(&cfg.TLSCACert, "tls.cacert", "", "CA certificate for client side TLS handshake") rootCmd.PersistentFlags().Uint64Var(&cfg.Gascap, "rpc.gascap", 50_000_000, "Sets a cap on gas that can be used in eth_call/estimateGas") rootCmd.PersistentFlags().Uint64Var(&cfg.MaxTraces, "trace.maxtraces", 200, "Sets a limit on traces that can be returned in trace_filter") @@ -110,17 +106,29 @@ func RootCommand() (*cobra.Command, *httpcfg.HttpCfg) { rootCmd.PersistentFlags().IntVar(&cfg.GRPCPort, "grpc.port", nodecfg.DefaultGRPCPort, "GRPC server listening port") rootCmd.PersistentFlags().BoolVar(&cfg.GRPCHealthCheckEnabled, "grpc.healthcheck", false, "Enable GRPC health check") rootCmd.PersistentFlags().Float64Var(ðconfig.Defaults.RPCTxFeeCap, utils.RPCGlobalTxFeeCapFlag.Name, utils.RPCGlobalTxFeeCapFlag.Value, utils.RPCGlobalTxFeeCapFlag.Usage) + rootCmd.PersistentFlags().StringVar(&cfg.TLSCertfile, "tls.cert", "", "certificate for client side TLS handshake for GRPC") + rootCmd.PersistentFlags().StringVar(&cfg.TLSKeyFile, "tls.key", "", "key file for client side TLS handshake for GRPC") + rootCmd.PersistentFlags().StringVar(&cfg.TLSCACert, "tls.cacert", "", "CA certificate for client side TLS handshake for GRPC") + + rootCmd.PersistentFlags().StringSliceVar(&cfg.API, "http.api", []string{"eth", "erigon"}, "API's offered over the RPC interface: eth,erigon,web3,net,debug,trace,txpool,db. Supported methods: https://github.com/ledgerwatch/erigon/tree/devel/cmd/rpcdaemon") rootCmd.PersistentFlags().BoolVar(&cfg.HttpServerEnabled, "http.enabled", true, "enable http server") + rootCmd.PersistentFlags().StringVar(&cfg.HttpListenAddress, "http.addr", nodecfg.DefaultHTTPHost, "HTTP server listening interface") + rootCmd.PersistentFlags().IntVar(&cfg.HttpPort, "http.port", nodecfg.DefaultHTTPPort, "HTTP server listening port") rootCmd.PersistentFlags().StringVar(&cfg.HttpURL, "http.url", "", "HTTP server listening url. will OVERRIDE http.addr and http.port. will NOT respect http paths. prefix supported are tcp, unix") - rootCmd.PersistentFlags().IntVar(&cfg.HttpPort, "http.port", nodecfg.DefaultHTTPPort, "HTTP-RPC server listening port") rootCmd.PersistentFlags().StringSliceVar(&cfg.HttpCORSDomain, "http.corsdomain", []string{}, "Comma separated list of domains from which to accept cross origin requests (browser enforced)") rootCmd.PersistentFlags().StringSliceVar(&cfg.HttpVirtualHost, "http.vhosts", nodecfg.DefaultConfig.HTTPVirtualHosts, "Comma separated list of virtual hostnames from which to accept requests (server enforced). Accepts '*' wildcard.") rootCmd.PersistentFlags().BoolVar(&cfg.HttpCompression, "http.compression", true, "Disable http compression") - rootCmd.PersistentFlags().StringSliceVar(&cfg.API, "http.api", []string{"eth", "erigon"}, "API's offered over the HTTP-RPC interface: eth,erigon,web3,net,debug,trace,txpool,db. Supported methods: https://github.com/ledgerwatch/erigon/tree/devel/cmd/rpcdaemon") - rootCmd.PersistentFlags().BoolVar(&cfg.WebsocketEnabled, "ws", false, "Enable Websockets - Same port as HTTP") + rootCmd.PersistentFlags().BoolVar(&cfg.WebsocketEnabled, "ws", false, "Enable Websockets - Same port as HTTP[S]") rootCmd.PersistentFlags().BoolVar(&cfg.WebsocketCompression, "ws.compression", false, "Enable Websocket compression (RFC 7692)") + rootCmd.PersistentFlags().BoolVar(&cfg.HttpsServerEnabled, "https.enabled", false, "enable http server") + rootCmd.PersistentFlags().StringVar(&cfg.HttpsListenAddress, "https.addr", nodecfg.DefaultHTTPHost, "rpc HTTPS server listening interface") + rootCmd.PersistentFlags().IntVar(&cfg.HttpsPort, "https.port", 0, "rpc HTTPS server listening port. default to http+363 if not set") + rootCmd.PersistentFlags().StringVar(&cfg.HttpsURL, "https.url", "", "rpc HTTPS server listening url. will OVERRIDE https.addr and https.port. will NOT respect paths. prefix supported are tcp, unix") + rootCmd.PersistentFlags().StringVar(&cfg.HttpsCertfile, "https.cert", "", "certificate for rpc HTTPS server") + rootCmd.PersistentFlags().StringVar(&cfg.HttpsKeyFile, "https.key", "", "key file for rpc HTTPS server") + rootCmd.PersistentFlags().BoolVar(&cfg.SocketServerEnabled, "socket.enabled", false, "Enable IPC server") rootCmd.PersistentFlags().StringVar(&cfg.SocketListenUrl, "socket.url", "unix:///var/run/erigon.sock", "IPC server listening url. prefix supported are tcp, unix") @@ -610,24 +618,25 @@ func startRegularRpcServer(ctx context.Context, cfg httpcfg.HttpCfg, rpcAPI []rp logger.Info("Socket Endpoint opened", "url", socketUrl) } + httpHandler := node.NewHTTPHandlerStack(srv, cfg.HttpCORSDomain, cfg.HttpVirtualHost, cfg.HttpCompression) + var wsHandler http.Handler + if cfg.WebsocketEnabled { + wsHandler = srv.WebsocketHandler([]string{"*"}, nil, cfg.WebsocketCompression, logger) + } + graphQLHandler := graphql.CreateHandler(defaultAPIList) + apiHandler, err := createHandler(cfg, defaultAPIList, httpHandler, wsHandler, graphQLHandler, nil) + if err != nil { + return err + } + if cfg.HttpServerEnabled { - httpHandler := node.NewHTTPHandlerStack(srv, cfg.HttpCORSDomain, cfg.HttpVirtualHost, cfg.HttpCompression) - var wsHandler http.Handler - if cfg.WebsocketEnabled { - wsHandler = srv.WebsocketHandler([]string{"*"}, nil, cfg.WebsocketCompression, logger) - } - - graphQLHandler := graphql.CreateHandler(defaultAPIList) - - apiHandler, err := createHandler(cfg, defaultAPIList, httpHandler, wsHandler, graphQLHandler, nil) - if err != nil { - return err - } httpEndpoint := fmt.Sprintf("tcp://%s:%d", cfg.HttpListenAddress, cfg.HttpPort) if cfg.HttpURL != "" { httpEndpoint = cfg.HttpURL } - listener, httpAddr, err := node.StartHTTPEndpoint(httpEndpoint, cfg.HTTPTimeouts, apiHandler) + listener, httpAddr, err := node.StartHTTPEndpoint(httpEndpoint, &node.HttpEndpointConfig{ + Timeouts: cfg.HTTPTimeouts, + }, apiHandler) if err != nil { return fmt.Errorf("could not start RPC api: %w", err) } @@ -639,6 +648,34 @@ func startRegularRpcServer(ctx context.Context, cfg httpcfg.HttpCfg, rpcAPI []rp logger.Info("HTTP endpoint closed", "url", httpAddr) }() } + if cfg.HttpsURL != "" { + cfg.HttpsServerEnabled = true + } + if cfg.HttpsServerEnabled { + if cfg.HttpsPort == 0 { + cfg.HttpsPort = cfg.HttpPort + 363 + } + httpsEndpoint := fmt.Sprintf("tcp://%s:%d", cfg.HttpsListenAddress, cfg.HttpsPort) + if cfg.HttpsURL != "" { + httpsEndpoint = cfg.HttpsURL + } + listener, httpAddr, err := node.StartHTTPEndpoint(httpsEndpoint, &node.HttpEndpointConfig{ + Timeouts: cfg.HTTPTimeouts, + HTTPS: true, + CertFile: cfg.HttpsCertfile, + KeyFile: cfg.HttpsKeyFile, + }, apiHandler) + if err != nil { + return fmt.Errorf("could not start RPC api: %w", err) + } + info = append(info, "https.url", httpAddr) + defer func() { + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _ = listener.Shutdown(shutdownCtx) + logger.Info("HTTPS endpoint closed", "url", httpAddr) + }() + } var ( healthServer *grpcHealth.Server @@ -797,7 +834,9 @@ func createEngineListener(cfg httpcfg.HttpCfg, engineApi []rpc.API, logger log.L return nil, nil, "", err } - engineListener, engineAddr, err := node.StartHTTPEndpoint(engineHttpEndpoint, cfg.AuthRpcTimeouts, engineApiHandler) + engineListener, engineAddr, err := node.StartHTTPEndpoint(engineHttpEndpoint, &node.HttpEndpointConfig{ + Timeouts: cfg.AuthRpcTimeouts, + }, engineApiHandler) if err != nil { return nil, nil, "", fmt.Errorf("could not start RPC api: %w", err) } diff --git a/cmd/rpcdaemon/cli/httpcfg/http_cfg.go b/cmd/rpcdaemon/cli/httpcfg/http_cfg.go index 408d3f82d..b2b2a99d0 100644 --- a/cmd/rpcdaemon/cli/httpcfg/http_cfg.go +++ b/cmd/rpcdaemon/cli/httpcfg/http_cfg.go @@ -30,6 +30,13 @@ type HttpCfg struct { AuthRpcVirtualHost []string HttpCompression bool + HttpsServerEnabled bool + HttpsURL string + HttpsListenAddress string + HttpsPort int + HttpsCertfile string + HttpsKeyFile string + AuthRpcPort int PrivateApiAddr string diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 95ccca559..e1e1164b0 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -305,7 +305,12 @@ var ( } HTTPEnabledFlag = cli.BoolFlag{ Name: "http", - Usage: "HTTP-RPC server (enabled by default). Use --http=false to disable it", + Usage: "JSON-RPC server (enabled by default). Use --http=false to disable it", + Value: true, + } + HTTPServerEnabledFlag = cli.BoolFlag{ + Name: "http.enabled", + Usage: "JSON-RPC HTTP server (enabled by default). Use --http.enabled=false to disable it", Value: true, } HTTPListenAddrFlag = cli.StringFlag{ diff --git a/node/endpoints.go b/node/endpoints.go index 8ca2e1696..5a2a10517 100644 --- a/node/endpoints.go +++ b/node/endpoints.go @@ -29,10 +29,73 @@ import ( "github.com/ledgerwatch/erigon/rpc" "github.com/ledgerwatch/erigon/rpc/rpccfg" "github.com/ledgerwatch/log/v3" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" ) +type HttpEndpointConfig struct { + Timeouts rpccfg.HTTPTimeouts + HTTPS bool + CertFile string + KeyFile string +} + // StartHTTPEndpoint starts the HTTP RPC endpoint. -func StartHTTPEndpoint(urlEndpoint string, timeouts rpccfg.HTTPTimeouts, handler http.Handler) (*http.Server, net.Addr, error) { +func StartHTTPEndpoint(urlEndpoint string, cfg *HttpEndpointConfig, handler http.Handler) (*http.Server, net.Addr, error) { + // start the HTTP listener + var ( + listener net.Listener + err error + ) + socketUrl, err := url.Parse(urlEndpoint) + if err != nil { + return nil, nil, fmt.Errorf("malformatted http listen url %s: %w", urlEndpoint, err) + } + if listener, err = net.Listen(socketUrl.Scheme, socketUrl.Host+socketUrl.EscapedPath()); err != nil { + return nil, nil, err + } + // make sure timeout values are meaningful + CheckTimeouts(&cfg.Timeouts) + // create the http2 server for handling h2c + h2 := &http2.Server{} + // enable h2c support + handler = h2c.NewHandler(handler, h2) + // Bundle the http server + httpSrv := &http.Server{ + Handler: handler, + ReadTimeout: cfg.Timeouts.ReadTimeout, + WriteTimeout: cfg.Timeouts.WriteTimeout, + IdleTimeout: cfg.Timeouts.IdleTimeout, + ReadHeaderTimeout: cfg.Timeouts.ReadTimeout, + } + // start the HTTP server + go func() { + var serveErr error + if cfg.HTTPS { + serveErr = httpSrv.ServeTLS(listener, cfg.CertFile, cfg.KeyFile) + if serveErr != nil && !isIgnoredHttpServerError(serveErr) { + log.Warn("Failed to serve https endpoint", "err", serveErr) + } + } else { + serveErr = httpSrv.Serve(listener) + if serveErr != nil && !isIgnoredHttpServerError(serveErr) { + log.Warn("Failed to serve http endpoint", "err", serveErr) + } + } + }() + return httpSrv, listener.Addr(), err +} + +func isIgnoredHttpServerError(serveErr error) bool { + return (errors.Is(serveErr, context.Canceled) || errors.Is(serveErr, libcommon.ErrStopped) || errors.Is(serveErr, http.ErrServerClosed)) + +} + +// StartHTTPEndpoint starts the HTTP RPC endpoint. +func StartHTTPSEndpoint(urlEndpoint string, + keyFile string, certFile string, + timeouts rpccfg.HTTPTimeouts, handler http.Handler, +) (*http.Server, net.Addr, error) { // start the HTTP listener var ( listener net.Listener @@ -47,7 +110,11 @@ func StartHTTPEndpoint(urlEndpoint string, timeouts rpccfg.HTTPTimeouts, handler } // make sure timeout values are meaningful CheckTimeouts(&timeouts) - // Bundle and start the HTTP server + // create the http2 server for handling h2c + h2 := &http2.Server{} + // enable h2c support + handler = h2c.NewHandler(handler, h2) + // Bundle the http server httpSrv := &http.Server{ Handler: handler, ReadTimeout: timeouts.ReadTimeout, @@ -55,8 +122,9 @@ func StartHTTPEndpoint(urlEndpoint string, timeouts rpccfg.HTTPTimeouts, handler IdleTimeout: timeouts.IdleTimeout, ReadHeaderTimeout: timeouts.ReadTimeout, } + // start the HTTP server go func() { - serveErr := httpSrv.Serve(listener) + serveErr := httpSrv.ServeTLS(listener, certFile, keyFile) if serveErr != nil && !(errors.Is(serveErr, context.Canceled) || errors.Is(serveErr, libcommon.ErrStopped) || errors.Is(serveErr, http.ErrServerClosed)) { log.Warn("Failed to serve http endpoint", "err", serveErr) diff --git a/turbo/cli/default_flags.go b/turbo/cli/default_flags.go index 40fad68e1..978b96d97 100644 --- a/turbo/cli/default_flags.go +++ b/turbo/cli/default_flags.go @@ -51,6 +51,7 @@ var DefaultFlags = []cli.Flag{ &BadBlockFlag, &utils.HTTPEnabledFlag, + &utils.HTTPServerEnabledFlag, &utils.GraphQLEnabledFlag, &utils.HTTPListenAddrFlag, &utils.HTTPPortFlag, diff --git a/turbo/cli/flags.go b/turbo/cli/flags.go index 43a903f0a..80bd6210f 100644 --- a/turbo/cli/flags.go +++ b/turbo/cli/flags.go @@ -2,9 +2,10 @@ package cli import ( "fmt" - "github.com/ledgerwatch/erigon-lib/common/hexutil" "time" + "github.com/ledgerwatch/erigon-lib/common/hexutil" + "github.com/ledgerwatch/erigon-lib/txpool/txpoolcfg" libcommon "github.com/ledgerwatch/erigon-lib/common" @@ -356,8 +357,9 @@ func setEmbeddedRpcDaemon(ctx *cli.Context, cfg *nodecfg.Config, logger log.Logg logger.Info("starting HTTP APIs", "APIs", apis) c := &httpcfg.HttpCfg{ - Enabled: ctx.Bool(utils.HTTPEnabledFlag.Name), - Dirs: cfg.Dirs, + Enabled: ctx.Bool(utils.HTTPEnabledFlag.Name), + HttpServerEnabled: ctx.Bool(utils.HTTPServerEnabledFlag.Name), + Dirs: cfg.Dirs, TLSKeyFile: cfg.TLSKeyFile, TLSCACert: cfg.TLSCACert,