From bdd5cf647c003ec58e5c7a683c5e0163bd27736a Mon Sep 17 00:00:00 2001 From: rene <41963722+renaynay@users.noreply.github.com> Date: Tue, 7 Jul 2020 13:39:04 +0200 Subject: [PATCH] refactor http servers + graphql (#26) --- cmd/geth/main.go | 2 - cmd/geth/usage.go | 4 +- cmd/utils/flags.go | 47 +++++++------------- cmd/utils/flags_legacy.go | 13 ++++++ go.mod | 2 +- go.sum | 15 +++++++ graphql/graphql_test.go | 10 +++-- graphql/service.go | 71 +++++++----------------------- node/api.go | 37 +++++----------- node/config.go | 20 +-------- node/defaults.go | 1 - node/node.go | 91 ++++++++++++++++++++++++--------------- node/node_test.go | 67 +++++++++++++++++++++++++--- node/rpcstack.go | 28 ++++-------- node/rpcstack_test.go | 2 +- 15 files changed, 208 insertions(+), 202 deletions(-) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index fbc8627498ce..fc135d9c5929 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -168,8 +168,6 @@ var ( utils.LegacyRPCCORSDomainFlag, utils.LegacyRPCVirtualHostsFlag, utils.GraphQLEnabledFlag, - utils.GraphQLListenAddrFlag, - utils.GraphQLPortFlag, utils.GraphQLCORSDomainFlag, utils.GraphQLVirtualHostsFlag, utils.HTTPApiFlag, diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index b80f05edb828..3ddf5c5002b2 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -140,8 +140,6 @@ var AppHelpFlagGroups = []flags.FlagGroup{ utils.WSApiFlag, utils.WSAllowedOriginsFlag, utils.GraphQLEnabledFlag, - utils.GraphQLListenAddrFlag, - utils.GraphQLPortFlag, utils.GraphQLCORSDomainFlag, utils.GraphQLVirtualHostsFlag, utils.RPCGlobalGasCap, @@ -229,6 +227,8 @@ var AppHelpFlagGroups = []flags.FlagGroup{ utils.LegacyWSApiFlag, utils.LegacyGpoBlocksFlag, utils.LegacyGpoPercentileFlag, + utils.LegacyGraphQLListenAddrFlag, + utils.LegacyGraphQLPortFlag, }, debug.DeprecatedFlags...), }, { diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index d97c01fdb5ac..e3554231fb6a 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -505,6 +505,20 @@ var ( Usage: "API's offered over the HTTP-RPC interface", Value: "", } + GraphQLEnabledFlag = cli.BoolFlag{ + Name: "graphql", + Usage: "Enable GraphQL on the HTTP-RPC server. Note that GraphQL can only be started if an HTTP server is started as well.", + } + GraphQLCORSDomainFlag = cli.StringFlag{ + Name: "graphql.corsdomain", + Usage: "Comma separated list of domains from which to accept cross origin requests (browser enforced)", + Value: "", + } + GraphQLVirtualHostsFlag = cli.StringFlag{ + Name: "graphql.vhosts", + Usage: "Comma separated list of virtual hostnames from which to accept requests (server enforced). Accepts '*' wildcard.", + Value: strings.Join(node.DefaultConfig.GraphQLVirtualHosts, ","), + } WSEnabledFlag = cli.BoolFlag{ Name: "ws", Usage: "Enable the WS-RPC server", @@ -529,30 +543,6 @@ var ( Usage: "Origins from which to accept websockets requests", Value: "", } - GraphQLEnabledFlag = cli.BoolFlag{ - Name: "graphql", - Usage: "Enable the GraphQL server", - } - GraphQLListenAddrFlag = cli.StringFlag{ - Name: "graphql.addr", - Usage: "GraphQL server listening interface", - Value: node.DefaultGraphQLHost, - } - GraphQLPortFlag = cli.IntFlag{ - Name: "graphql.port", - Usage: "GraphQL server listening port", - Value: node.DefaultGraphQLPort, - } - GraphQLCORSDomainFlag = cli.StringFlag{ - Name: "graphql.corsdomain", - Usage: "Comma separated list of domains from which to accept cross origin requests (browser enforced)", - Value: "", - } - GraphQLVirtualHostsFlag = cli.StringFlag{ - Name: "graphql.vhosts", - Usage: "Comma separated list of virtual hostnames from which to accept requests (server enforced). Accepts '*' wildcard.", - Value: strings.Join(node.DefaultConfig.GraphQLVirtualHosts, ","), - } ExecFlag = cli.StringFlag{ Name: "exec", Usage: "Execute JavaScript statement", @@ -939,13 +929,6 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) { // setGraphQL creates the GraphQL listener interface string from the set // command line flags, returning empty if the GraphQL endpoint is disabled. func setGraphQL(ctx *cli.Context, cfg *node.Config) { - if ctx.GlobalBool(GraphQLEnabledFlag.Name) && cfg.GraphQLHost == "" { - cfg.GraphQLHost = "127.0.0.1" - if ctx.GlobalIsSet(GraphQLListenAddrFlag.Name) { - cfg.GraphQLHost = ctx.GlobalString(GraphQLListenAddrFlag.Name) - } - } - cfg.GraphQLPort = ctx.GlobalInt(GraphQLPortFlag.Name) if ctx.GlobalIsSet(GraphQLCORSDomainFlag.Name) { cfg.GraphQLCors = splitAndTrim(ctx.GlobalString(GraphQLCORSDomainFlag.Name)) } @@ -1691,7 +1674,7 @@ func RegisterEthStatsService(stack *node.Node, backend ethapi.Backend, url strin // RegisterGraphQLService is a utility function to construct a new service and register it against a node. func RegisterGraphQLService(stack *node.Node, backend ethapi.Backend, cfg node.Config) { - if err := graphql.New(stack, backend, cfg.GraphQLEndpoint(), cfg.GraphQLCors, cfg.GraphQLVirtualHosts, cfg.HTTPTimeouts); err != nil { + if err := graphql.New(stack, backend, cfg.GraphQLCors, cfg.GraphQLVirtualHosts); err != nil { Fatalf("Failed to register the GraphQL service: %v", err) } } diff --git a/cmd/utils/flags_legacy.go b/cmd/utils/flags_legacy.go index c39664d24984..1376d47c0554 100644 --- a/cmd/utils/flags_legacy.go +++ b/cmd/utils/flags_legacy.go @@ -89,6 +89,8 @@ var ( Name: "testnet", Usage: "Pre-configured test network (Deprecated: Please choose one of --goerli, --rinkeby, or --ropsten.)", } + + // (Deprecated May 2020, shown in aliased flags section) LegacyRPCEnabledFlag = cli.BoolFlag{ Name: "rpc", Usage: "Enable the HTTP-RPC server (deprecated, use --http)", @@ -158,6 +160,17 @@ var ( Usage: "Comma separated enode URLs for P2P v5 discovery bootstrap (light server, light nodes) (deprecated, use --bootnodes)", Value: "", } + + // (Deprecated July 2020, shown in aliased flags section) + LegacyGraphQLListenAddrFlag = cli.StringFlag{ + Name: "graphql.addr", + Usage: "GraphQL server listening interface (deprecated, graphql can only be enabled on the HTTP-RPC server endpoint, use --graphql)", + } + LegacyGraphQLPortFlag = cli.IntFlag{ + Name: "graphql.port", + Usage: "GraphQL server listening port (deprecated, graphql can only be enabled on the HTTP-RPC server endpoint, use --graphql)", + Value: node.DefaultHTTPPort, + } ) // showDeprecated displays deprecated flags that will be soon removed from the codebase. diff --git a/go.mod b/go.mod index 0ca0709049e5..3ce576a3cb34 100644 --- a/go.mod +++ b/go.mod @@ -60,7 +60,7 @@ require ( github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect - golang.org/x/sync v0.0.0-20181108010431-42b317875d0f + golang.org/x/sync v0.0.0-20190423024810-112230192c58 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd golang.org/x/text v0.3.2 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 diff --git a/go.sum b/go.sum index 3f087e92e1e5..925cc459f8c2 100644 --- a/go.sum +++ b/go.sum @@ -193,30 +193,45 @@ github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208 h1:1cngl9mPEoITZG8s8cVcUy5CeIBYhEESkOB7m6Gmkrk= github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208/go.mod h1:IotVbo4F+mw0EzQ08zFqg7pK3FebNXpaMsRy2RT+Ees= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= +gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6 h1:a6cXbcDDUkSBlpnkWV1bJ+vv3mOgQEltEJ2rPxroVu0= gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/graphql/graphql_test.go b/graphql/graphql_test.go index 08c1e4c8b576..366685151014 100644 --- a/graphql/graphql_test.go +++ b/graphql/graphql_test.go @@ -25,14 +25,16 @@ import ( "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/rpc" - "github.com/stretchr/testify/assert" ) func TestBuildSchema(t *testing.T) { + stack, err := node.New(&node.DefaultConfig) + if err != nil { + t.Fatalf("could not create new node: %v", err) + } // Make sure the schema can be parsed and matched up to the object model. - if _, err := newHandler(nil); err != nil { + if err := newHandler(stack, nil, []string{}, []string{}); err != nil { t.Errorf("Could not construct GraphQL handler: %v", err) } } @@ -120,7 +122,7 @@ func createGQLService(t *testing.T, stack *node.Node, endpoint string) { } // create gql service - err = New(stack, ethBackend.APIBackend, endpoint, []string{}, []string{}, rpc.DefaultHTTPTimeouts) + err = New(stack, ethBackend.APIBackend,[]string{}, []string{}) if err != nil { t.Fatalf("could not create graphql service: %v", err) } diff --git a/graphql/service.go b/graphql/service.go index 5c3d872842d9..72ecf35bcb22 100644 --- a/graphql/service.go +++ b/graphql/service.go @@ -17,81 +17,42 @@ package graphql import ( + "fmt" "github.com/ethereum/go-ethereum/internal/ethapi" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/rpc" "github.com/graph-gophers/graphql-go" "github.com/graph-gophers/graphql-go/relay" - "net/http" ) // New constructs a new GraphQL service instance. -func New(stack *node.Node, backend ethapi.Backend, endpoint string, cors, vhosts []string, timeouts rpc.HTTPTimeouts) error { +func New(stack *node.Node, backend ethapi.Backend, cors, vhosts []string) error { if backend == nil { stack.Fatalf("missing backend") } // check if http server with given endpoint exists and enable graphQL on it - server := stack.ExistingHTTPServer(endpoint) - if server != nil { - // set vhosts, cors and timeouts - server.Vhosts = append(server.Vhosts, vhosts...) - server.CorsAllowedOrigins = append(server.CorsAllowedOrigins, cors...) - server.Timeouts = timeouts - // create handler - handler, err := createHandler(backend, cors, vhosts) - if err != nil { - return err - } - server.GQLHandler = handler - // don't register lifecycle if registering on existing http server - return nil - } - // otherwise create a new server - handler, err := createHandler(backend, cors, vhosts) - if err != nil { - return err - } - // create the http server - gqlServer := &node.HTTPServer{ - RPCAllowed: 0, - WSAllowed: 0, - Vhosts: vhosts, - CorsAllowedOrigins: cors, - Timeouts: timeouts, - GQLHandler: handler, - Srv: rpc.NewServer(), - } - gqlServer.SetEndpoint(endpoint) - stack.RegisterHTTPServer(endpoint, gqlServer) - - return nil -} - -func createHandler(backend ethapi.Backend, cors, vhosts []string) (http.Handler, error) { - // create handler stack and wrap the graphql handler - handler, err := newHandler(backend) - if err != nil { - return nil, err - } - handler = node.NewHTTPHandlerStack(handler, cors, vhosts) - - return handler, nil + return newHandler(stack, backend, cors, vhosts) } // newHandler returns a new `http.Handler` that will answer GraphQL queries. // It additionally exports an interactive query browser on the / endpoint. -func newHandler(backend ethapi.Backend) (http.Handler, error) { +func newHandler(stack *node.Node, backend ethapi.Backend, cors, vhosts []string) error { q := Resolver{backend} s, err := graphql.ParseSchema(schema, &q) if err != nil { - return nil, err + return err } h := &relay.Handler{Schema: s} + handler := node.NewHTTPHandlerStack(h, cors, vhosts) + + endpoint := stack.RegisterPath("/graphql/ui", GraphiQL{}) + endpoint = stack.RegisterPath("/graphql", handler) + endpoint = stack.RegisterPath("/graphql/", handler) - mux := http.NewServeMux() - mux.Handle("/", GraphiQL{}) - mux.Handle("/graphql", h) - mux.Handle("/graphql/", h) - return mux, nil + if endpoint != "" { + log.Info("GraphQL configured on endpoint", "endpoint", fmt.Sprintf("http://%s/graphql", endpoint)) + log.Info("GraphQL web UI enabled", "endpoint", fmt.Sprintf("http://%s/graphql/ui", endpoint)) + } + return nil } diff --git a/node/api.go b/node/api.go index 17adc19695b6..55956f63ded0 100644 --- a/node/api.go +++ b/node/api.go @@ -191,7 +191,7 @@ func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis } } // configure http server - httpServer := &HTTPServer{ + httpServer := &httpServer{ host: *host, port: *port, endpoint: endpoint, @@ -201,7 +201,8 @@ func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis Whitelist: modules, } // create handler - httpServer.handler = NewHTTPHandlerStack(httpServer.Srv, httpServer.CorsAllowedOrigins, httpServer.Vhosts) + handler := NewHTTPHandlerStack(httpServer.Srv, httpServer.CorsAllowedOrigins, httpServer.Vhosts) + httpServer.srvMux.Handle("/", handler) // create HTTP server if err := api.node.CreateHTTPServer(httpServer, false); err != nil { return false, err @@ -213,7 +214,6 @@ func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis api.node.log.Info("HTTP endpoint opened", "url", fmt.Sprintf("http://%v/", httpServer.Listener.Addr()), "cors", strings.Join(httpServer.CorsAllowedOrigins, ","), "vhosts", strings.Join(httpServer.Vhosts, ",")) - return true, nil } @@ -228,7 +228,6 @@ func (api *PrivateAdminAPI) StopRPC() (bool, error) { return true, nil } } - return false, fmt.Errorf("HTTP RPC not running") } @@ -255,23 +254,6 @@ func (api *PrivateAdminAPI) StartWS(host *string, port *int, allowedOrigins *str port = &api.node.config.WSPort } endpoint := fmt.Sprintf("%s:%d", *host, *port) - // check if there is an existing server on the specified port, and if there is, enable ws on it - if server, exists := api.node.httpServers[endpoint]; exists { - // else configure ws on the existing server - atomic.StoreInt32(&server.WSAllowed, 1) - // configure origins - origins := api.node.config.WSOrigins - if allowedOrigins != nil { - origins = nil - for _, origin := range strings.Split(*allowedOrigins, ",") { - origins = append(origins, strings.TrimSpace(origin)) - } - } - server.WsOrigins = origins - - api.node.log.Info("WebSocket endpoint opened", "url", fmt.Sprintf("ws://%v", server.Listener.Addr())) - return true, nil - } origins := api.node.config.WSOrigins if allowedOrigins != nil { @@ -286,6 +268,8 @@ func (api *PrivateAdminAPI) StartWS(host *string, port *int, allowedOrigins *str atomic.StoreInt32(&existingServer.WSAllowed, 1) existingServer.WsOrigins = origins + api.node.log.Info("WebSocket endpoint opened", "url", fmt.Sprintf("ws://%v", existingServer.Listener.Addr())) + return true, nil } modules := api.node.config.WSModules @@ -296,7 +280,7 @@ func (api *PrivateAdminAPI) StartWS(host *string, port *int, allowedOrigins *str } } // configure http server - wsServer := &HTTPServer{ + wsServer := &httpServer{ Srv: rpc.NewServer(), endpoint: endpoint, host: *host, @@ -306,7 +290,8 @@ func (api *PrivateAdminAPI) StartWS(host *string, port *int, allowedOrigins *str WSAllowed: 1, } // create handler - wsServer.handler = wsServer.Srv.WebsocketHandler(wsServer.WsOrigins) + handler := wsServer.Srv.WebsocketHandler(wsServer.WsOrigins) + wsServer.srvMux.Handle("/", handler) // create the HTTP server if err := api.node.CreateHTTPServer(wsServer, api.node.config.WSExposeAll); err != nil { return false, err @@ -314,9 +299,10 @@ func (api *PrivateAdminAPI) StartWS(host *string, port *int, allowedOrigins *str // register the HTTP server api.node.RegisterHTTPServer(endpoint, wsServer) // start the HTTP server - wsServer.Start() + if err := wsServer.Start(); err != nil { + return false, err + } api.node.log.Info("WebSocket endpoint opened", "url", fmt.Sprintf("ws://%v", wsServer.Listener.Addr())) - return true, nil } @@ -334,7 +320,6 @@ func (api *PrivateAdminAPI) StopWS() (bool, error) { return true, nil } } - return false, fmt.Errorf("WebSocket RPC not running") } diff --git a/node/config.go b/node/config.go index 61566b7bee9b..55532632cd1b 100644 --- a/node/config.go +++ b/node/config.go @@ -162,15 +162,6 @@ type Config struct { // private APIs to untrusted users is a major security risk. WSExposeAll bool `toml:",omitempty"` - // GraphQLHost is the host interface on which to start the GraphQL server. If this - // field is empty, no GraphQL API endpoint will be started. - GraphQLHost string - - // GraphQLPort is the TCP port number on which to start the GraphQL server. The - // default zero value is/ valid and will pick a port number randomly (useful - // for ephemeral nodes). - GraphQLPort int `toml:",omitempty"` - // GraphQLCors is the Cross-Origin Resource Sharing header to send to requesting // clients. Please be aware that CORS is a browser enforced security, it's fully // useless for custom HTTP clients. @@ -247,15 +238,6 @@ func (c *Config) HTTPEndpoint() string { return fmt.Sprintf("%s:%d", c.HTTPHost, c.HTTPPort) } -// GraphQLEndpoint resolves a GraphQL endpoint based on the configured host interface -// and port parameters. -func (c *Config) GraphQLEndpoint() string { - if c.GraphQLHost == "" { - return "" - } - return fmt.Sprintf("%s:%d", c.GraphQLHost, c.GraphQLPort) -} - // DefaultHTTPEndpoint returns the HTTP endpoint used by default. func DefaultHTTPEndpoint() string { config := &Config{HTTPHost: DefaultHTTPHost, HTTPPort: DefaultHTTPPort} @@ -280,7 +262,7 @@ func DefaultWSEndpoint() string { // ExtRPCEnabled returns the indicator whether node enables the external // RPC(http, ws or graphql). func (c *Config) ExtRPCEnabled() bool { - return c.HTTPHost != "" || c.WSHost != "" || c.GraphQLHost != "" + return c.HTTPHost != "" || c.WSHost != "" } // NodeName returns the devp2p node identifier. diff --git a/node/defaults.go b/node/defaults.go index f84a5d547998..c685dde5d127 100644 --- a/node/defaults.go +++ b/node/defaults.go @@ -45,7 +45,6 @@ var DefaultConfig = Config{ HTTPTimeouts: rpc.DefaultHTTPTimeouts, WSPort: DefaultWSPort, WSModules: []string{"net", "web3"}, - GraphQLPort: DefaultGraphQLPort, GraphQLVirtualHosts: []string{"localhost"}, P2P: p2p.Config{ ListenAddr: ":30303", diff --git a/node/node.go b/node/node.go index f9bc79e5688f..af0c71b1bc3a 100644 --- a/node/node.go +++ b/node/node.go @@ -58,7 +58,7 @@ type Node struct { httpServers serverMap // serverMap stores information about the node's rpc, ws, and graphQL http servers. inprocHandler *rpc.Server // In-process RPC request handler to process the API requests rpcAPIs []rpc.API // List of APIs currently provided by the node - ipc *HTTPServer // Stores information about the ipc http server + ipc *httpServer // Stores information about the ipc http server } // New creates a new P2P node, ready for protocol registration. @@ -102,7 +102,7 @@ func New(conf *Config) (*Node, error) { config: conf, lifecycles: make(map[reflect.Type]Lifecycle), httpServers: make(serverMap), - ipc: &HTTPServer{ + ipc: &httpServer{ endpoint: conf.IPCEndpoint(), }, eventmux: new(event.TypeMux), @@ -127,7 +127,7 @@ func New(conf *Config) (*Node, error) { // Configure HTTP server(s) if conf.HTTPHost != "" { - httpServ := &HTTPServer{ + httpServ := &httpServer{ CorsAllowedOrigins: conf.HTTPCors, Vhosts: conf.HTTPVirtualHosts, Whitelist: conf.HTTPModules, @@ -150,7 +150,7 @@ func New(conf *Config) (*Node, error) { node.httpServers[conf.HTTPEndpoint()] = httpServ } if conf.WSHost != "" { - node.httpServers[conf.WSEndpoint()] = &HTTPServer{ + node.httpServers[conf.WSEndpoint()] = &httpServer{ WsOrigins: conf.WSOrigins, Whitelist: conf.WSModules, Srv: rpc.NewServer(), @@ -187,7 +187,7 @@ func (n *Node) Close() error { } } -// RegisterLifecycle registers the given Lifecycle on the node +// RegisterLifecycle registers the given Lifecycle on the node. func (n *Node) RegisterLifecycle(lifecycle Lifecycle) { kind := reflect.TypeOf(lifecycle) if _, exists := n.lifecycles[kind]; exists { @@ -197,31 +197,43 @@ func (n *Node) RegisterLifecycle(lifecycle Lifecycle) { n.lifecycles[kind] = lifecycle } -// RegisterProtocols adds backend's protocols to the node's p2p server +// RegisterProtocols adds backend's protocols to the node's p2p server. func (n *Node) RegisterProtocols(protocols []p2p.Protocol) { n.server.Protocols = append(n.server.Protocols, protocols...) } -// RegisterAPIs registers the APIs a service provides on the node +// RegisterAPIs registers the APIs a service provides on the node. func (n *Node) RegisterAPIs(apis []rpc.API) { n.rpcAPIs = append(n.rpcAPIs, apis...) } -// RegisterHTTPServer registers the given HTTP server on the node -func (n *Node) RegisterHTTPServer(endpoint string, server *HTTPServer) { +// RegisterHTTPServer registers the given HTTP server on the node. +func (n *Node) RegisterHTTPServer(endpoint string, server *httpServer) { n.httpServers[endpoint] = server } -// ExistingHTTPServer checks if an HTTP server is already configured on the given endpoint -func (n *Node) ExistingHTTPServer(endpoint string) *HTTPServer { +// RegisterPath mounts the given handler on the given path on the canonical HTTP server. +func (n *Node) RegisterPath(path string, handler http.Handler) string { + for _, server := range n.httpServers { + if atomic.LoadInt32(&server.RPCAllowed) == 1 { + server.srvMux.Handle(path, handler) + return server.endpoint + } + } + n.log.Warn(fmt.Sprintf("HTTP server not configured on node, path %s cannot be enabled", path)) + return "" +} + +// ExistingHTTPServer checks if an HTTP server is already configured on the given endpoint. +func (n *Node) ExistingHTTPServer(endpoint string) *httpServer { if server, exists := n.httpServers[endpoint]; exists { return server } return nil } -// CreateHTTPServer creates an http.Server and adds it to the given HTTPServer -func (n *Node) CreateHTTPServer(h *HTTPServer, exposeAll bool) error { +// CreateHTTPServer creates an http.Server and adds it to the given httpServer. +func (n *Node) CreateHTTPServer(h *httpServer, exposeAll bool) error { // register apis and create handler stack err := RegisterApisFromWhitelist(n.rpcAPIs, h.Whitelist, h.Srv, exposeAll) if err != nil { @@ -234,7 +246,7 @@ func (n *Node) CreateHTTPServer(h *HTTPServer, exposeAll bool) error { return err } // create the HTTP server - httpSrv := &http.Server{Handler: h.handler} + httpSrv := &http.Server{Handler: &h.srvMux} // check timeouts if they exist if h.Timeouts != (rpc.HTTPTimeouts{}) { CheckTimeouts(&h.Timeouts) @@ -242,14 +254,14 @@ func (n *Node) CreateHTTPServer(h *HTTPServer, exposeAll bool) error { httpSrv.WriteTimeout = h.Timeouts.WriteTimeout httpSrv.IdleTimeout = h.Timeouts.IdleTimeout } - // add listener and http.Server to HTTPServer + // add listener and http.Server to httpServer h.Listener = listener h.Server = httpSrv return nil } -// running returns true if the node's p2p server is already running +// running returns true if the node's p2p server is already running. func (n *Node) running() bool { return n.server.Running() } @@ -298,7 +310,7 @@ func (n *Node) Start() error { return nil } -// stopLifecycles stops the node's running Lifecycles +// stopLifecycles stops the node's running Lifecycles. func (n *Node) stopLifecycles(started []Lifecycle) { for _, lifecycle := range started { lifecycle.Stop() @@ -343,23 +355,12 @@ func (n *Node) configureRPC() error { n.stopInProc() return err } - + // configure HTTPServers for _, server := range n.httpServers { // configure the handlers - if atomic.LoadInt32(&server.RPCAllowed) == 1 { - server.handler = NewHTTPHandlerStack(server.Srv, server.CorsAllowedOrigins, server.Vhosts) - // wrap ws handler just in case ws is enabled through the console after start-up - wsHandler := server.Srv.WebsocketHandler(server.WsOrigins) - server.handler = server.NewWebsocketUpgradeHandler(server.handler, wsHandler) - - n.log.Info("HTTP configured on endpoint ", "endpoint", server.endpoint) - if atomic.LoadInt32(&server.WSAllowed) == 1 { - n.log.Info("Websocket configured on endpoint ", "endpoint", server.endpoint) - } - } - if (atomic.LoadInt32(&server.WSAllowed) == 1) && server.handler == nil { - server.handler = server.Srv.WebsocketHandler(server.WsOrigins) - n.log.Info("Websocket configured on endpoint ", "endpoint", server.endpoint) + handler := n.createHandler(server) + if handler != nil { + server.srvMux.Handle("/", handler) } // create the HTTP server if err := n.CreateHTTPServer(server, false); err != nil { @@ -374,6 +375,28 @@ func (n *Node) configureRPC() error { return nil } +// createHandler creates the http.Handler for the given httpServer. +func (n *Node) createHandler(server *httpServer) http.Handler { + var handler http.Handler + if atomic.LoadInt32(&server.RPCAllowed) == 1 { + handler = NewHTTPHandlerStack(server.Srv, server.CorsAllowedOrigins, server.Vhosts) + // wrap ws handler just in case ws is enabled through the console after start-up + wsHandler := server.Srv.WebsocketHandler(server.WsOrigins) + handler = server.NewWebsocketUpgradeHandler(handler, wsHandler) + + n.log.Info("HTTP configured on endpoint ", "endpoint", fmt.Sprintf("http://%s/", server.endpoint)) + if atomic.LoadInt32(&server.WSAllowed) == 1 { + n.log.Info("Websocket configured on endpoint ", "endpoint", fmt.Sprintf("ws://%s/", server.endpoint)) + } + } + if (atomic.LoadInt32(&server.WSAllowed) == 1) && handler == nil { + handler = server.Srv.WebsocketHandler(server.WsOrigins) + n.log.Info("Websocket configured on endpoint ", "endpoint", fmt.Sprintf("ws://%s/", server.endpoint)) + } + + return handler +} + // startInProc initializes an in-process RPC endpoint. func (n *Node) startInProc() error { // Register all the APIs exposed by the services @@ -426,7 +449,7 @@ func (n *Node) stopIPC() { } // stopServers terminates the given HTTP servers' endpoints -func (n *Node) stopServer(server *HTTPServer) { +func (n *Node) stopServer(server *httpServer) { if server.Server != nil { url := fmt.Sprintf("http://%v/", server.Listener.Addr()) // Don't bother imposing a timeout here. @@ -437,7 +460,7 @@ func (n *Node) stopServer(server *HTTPServer) { server.Srv.Stop() server.Srv = nil } - // remove stopped http server from node's http servers // TODO is this preferable? + // remove stopped http server from node's http servers delete(n.httpServers, server.endpoint) } diff --git a/node/node_test.go b/node/node_test.go index bb1543650894..f417eb18215d 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -19,6 +19,7 @@ package node import ( "errors" "fmt" + "io" "io/ioutil" "net/http" "os" @@ -365,7 +366,7 @@ func TestLifecycleTerminationGuarantee(t *testing.T) { stack.server.PrivateKey = testNodeKey } -// Tests whether a given HTTPServer can be registered on the node +// Tests whether a given httpServer can be registered on the node func TestRegisterHTTPServer(t *testing.T) { stack, err := New(testNodeConfig()) if err != nil { @@ -373,21 +374,21 @@ func TestRegisterHTTPServer(t *testing.T) { } defer stack.Close() - srv1 := &HTTPServer{ + srv1 := &httpServer{ host: "test1", port: 0001, } endpoint1 := fmt.Sprintf("%s:%d", srv1.host, srv1.port) stack.RegisterHTTPServer(endpoint1, srv1) - srv2 := &HTTPServer{ + srv2 := &httpServer{ host: "test2", port: 0002, } endpoint2 := fmt.Sprintf("%s:%d", srv2.host, srv2.port) stack.RegisterHTTPServer(endpoint2, srv2) - noop := &HTTPServer{ + noop := &httpServer{ host: "test", port: 0000, } @@ -404,6 +405,55 @@ func TestRegisterHTTPServer(t *testing.T) { } } +// Tests whether a handler can be successfully mounted on the canonical HTTP server +// on the given path +func TestRegisterPath_Successful(t *testing.T) { + node := createNode(t, 7878, 7979) + + // create and mount handler + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("success")) + }) + endpoint := node.RegisterPath("/test", handler) + assert.Equal(t, "127.0.0.1:7878", endpoint) + + // start node + if err := node.Start(); err != nil { + t.Fatalf("could not start node: %v", err) + } + + // create HTTP request + httpReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:7878/test", nil) + if err != nil { + t.Error("could not issue new http request ", err) + } + + // check response + resp := doHTTPRequest(t, httpReq) + buf := make([]byte, 7) + _, err = io.ReadFull(resp.Body, buf) + if err != nil { + t.Fatalf("could not read response: %v", err) + } + assert.Equal(t, "success", string(buf)) +} + +// Tests that the given handler will not be successfully mounted since no HTTP server +// is enabled for RPC +func TestRegisterPath_Unsuccessful(t *testing.T) { + node, err := New(&DefaultConfig) + if err != nil { + t.Fatalf("could not create new node: %v", err) + } + + // create and mount handler + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("success")) + }) + endpoint := node.RegisterPath("/test", handler) + assert.Equal(t, "", endpoint) +} + // Tests whether a node can successfully create and register HTTP server // lifecycles on the node. func TestHTTPServerCreateAndStop(t *testing.T) { @@ -496,7 +546,7 @@ func TestWebsocketHTTPOnSamePort_HTTPRequest(t *testing.T) { assert.Equal(t, "gzip", resp.Header.Get("Content-Encoding")) } -func startHTTP(t *testing.T, httpPort, wsPort int) *Node { +func createNode(t *testing.T, httpPort, wsPort int) *Node { conf := &Config{ HTTPHost: "127.0.0.1", HTTPPort: httpPort, @@ -507,7 +557,12 @@ func startHTTP(t *testing.T, httpPort, wsPort int) *Node { if err != nil { t.Fatalf("could not create a new node: %v", err) } - err = node.Start() + return node +} + +func startHTTP(t *testing.T, httpPort, wsPort int) *Node { + node := createNode(t, httpPort, wsPort) + err := node.Start() if err != nil { t.Fatalf("could not start http service on node: %v", err) } diff --git a/node/rpcstack.go b/node/rpcstack.go index cb39a1c4f822..fe2d2673af71 100644 --- a/node/rpcstack.go +++ b/node/rpcstack.go @@ -33,9 +33,11 @@ import ( "github.com/rs/cors" ) -type serverMap map[string]*HTTPServer // Stores information about all http servers (if any) by their endpoint, including http, ws, and graphql +type serverMap map[string]*httpServer // Stores information about all http servers (if any) by their endpoint, including http, ws, and graphql + +type httpServer struct { + srvMux http.ServeMux -type HTTPServer struct { handler http.Handler Srv *rpc.Server Server *http.Server @@ -55,8 +57,6 @@ type HTTPServer struct { RPCAllowed int32 WSAllowed int32 - - GQLHandler http.Handler } func (sm serverMap) Start() error { @@ -77,15 +77,15 @@ func (sm serverMap) Stop() error { return nil } -// Start starts the serverMap's HTTP server. // TODO I don't like the way this is written -func (h *HTTPServer) Start() error { +// Start starts the httpServer's http.Server +func (h *httpServer) Start() error { go h.Server.Serve(h.Listener) log.Info("HTTP endpoint successfully opened", "url", fmt.Sprintf("http://%v/", h.Listener.Addr())) return nil } -// Stop shuts down the serverMap's HTTP server. // TODO I don't like the way this is written -func (h *HTTPServer) Stop() error { +// Stop shuts down the httpServer's http.Server +func (h *httpServer) Stop() error { if h.Server != nil { url := fmt.Sprintf("http://%v/", h.Listener.Addr()) // Don't bother imposing a timeout here. @@ -100,16 +100,6 @@ func (h *HTTPServer) Stop() error { return nil } -// SetHandler assigns the given handler to the serverMap. -func (h *HTTPServer) SetHandler(handler http.Handler) { - h.handler = handler -} - -// SetEndpoints assigns the given endpoint to the serverMap. -func (h *HTTPServer) SetEndpoint(endpoint string) { - h.endpoint = endpoint -} - // NewHTTPHandlerStack returns wrapped http-related handlers func NewHTTPHandlerStack(srv http.Handler, cors []string, vhosts []string) http.Handler { // Wrap the CORS-handler within a host-handler @@ -221,7 +211,7 @@ func newGzipHandler(next http.Handler) http.Handler { // NewWebsocketUpgradeHandler returns a websocket handler that serves an incoming request only if it contains an upgrade // request to the websocket protocol. If not, serves the the request with the http handler. -func (hs *HTTPServer) NewWebsocketUpgradeHandler(h http.Handler, ws http.Handler) http.Handler { +func (hs *httpServer) NewWebsocketUpgradeHandler(h http.Handler, ws http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if atomic.LoadInt32(&hs.WSAllowed) == 1 && isWebsocket(r) { ws.ServeHTTP(w, r) diff --git a/node/rpcstack_test.go b/node/rpcstack_test.go index f745e350afe9..3dfaf53ed6bc 100644 --- a/node/rpcstack_test.go +++ b/node/rpcstack_test.go @@ -12,7 +12,7 @@ import ( ) func TestNewWebsocketUpgradeHandler_websocket(t *testing.T) { - h := &HTTPServer{ + h := &httpServer{ Srv: rpc.NewServer(), WSAllowed: 1, }