Skip to content

Commit

Permalink
cmd/geth, node: allow configuring JSON-RPC on custom path prefix (#22184
Browse files Browse the repository at this point in the history
)

This change allows users to set a custom path prefix on which to mount the http-rpc
or ws-rpc handlers via the new flags --http.rpcprefix and --ws.rpcprefix.

Fixes #21826

Co-authored-by: Felix Lange <fjl@twurst.com>
  • Loading branch information
renaynay and fjl authored Feb 2, 2021
1 parent e3430ac commit 4eae0c6
Show file tree
Hide file tree
Showing 11 changed files with 320 additions and 65 deletions.
2 changes: 2 additions & 0 deletions cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ var (
utils.GraphQLCORSDomainFlag,
utils.GraphQLVirtualHostsFlag,
utils.HTTPApiFlag,
utils.HTTPPathPrefixFlag,
utils.LegacyRPCApiFlag,
utils.WSEnabledFlag,
utils.WSListenAddrFlag,
Expand All @@ -190,6 +191,7 @@ var (
utils.WSApiFlag,
utils.LegacyWSApiFlag,
utils.WSAllowedOriginsFlag,
utils.WSPathPrefixFlag,
utils.LegacyWSAllowedOriginsFlag,
utils.IPCDisabledFlag,
utils.IPCPathFlag,
Expand Down
2 changes: 2 additions & 0 deletions cmd/geth/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,14 @@ var AppHelpFlagGroups = []flags.FlagGroup{
utils.HTTPListenAddrFlag,
utils.HTTPPortFlag,
utils.HTTPApiFlag,
utils.HTTPPathPrefixFlag,
utils.HTTPCORSDomainFlag,
utils.HTTPVirtualHostsFlag,
utils.WSEnabledFlag,
utils.WSListenAddrFlag,
utils.WSPortFlag,
utils.WSApiFlag,
utils.WSPathPrefixFlag,
utils.WSAllowedOriginsFlag,
utils.GraphQLEnabledFlag,
utils.GraphQLCORSDomainFlag,
Expand Down
18 changes: 18 additions & 0 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,11 @@ var (
Usage: "API's offered over the HTTP-RPC interface",
Value: "",
}
HTTPPathPrefixFlag = cli.StringFlag{
Name: "http.rpcprefix",
Usage: "HTTP path path prefix on which JSON-RPC is served. Use '/' to serve on all paths.",
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.",
Expand Down Expand Up @@ -569,6 +574,11 @@ var (
Usage: "Origins from which to accept websockets requests",
Value: "",
}
WSPathPrefixFlag = cli.StringFlag{
Name: "ws.rpcprefix",
Usage: "HTTP path prefix on which JSON-RPC is served. Use '/' to serve on all paths.",
Value: "",
}
ExecFlag = cli.StringFlag{
Name: "exec",
Usage: "Execute JavaScript statement",
Expand Down Expand Up @@ -946,6 +956,10 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) {
if ctx.GlobalIsSet(HTTPVirtualHostsFlag.Name) {
cfg.HTTPVirtualHosts = SplitAndTrim(ctx.GlobalString(HTTPVirtualHostsFlag.Name))
}

if ctx.GlobalIsSet(HTTPPathPrefixFlag.Name) {
cfg.HTTPPathPrefix = ctx.GlobalString(HTTPPathPrefixFlag.Name)
}
}

// setGraphQL creates the GraphQL listener interface string from the set
Expand Down Expand Up @@ -995,6 +1009,10 @@ func setWS(ctx *cli.Context, cfg *node.Config) {
if ctx.GlobalIsSet(WSApiFlag.Name) {
cfg.WSModules = SplitAndTrim(ctx.GlobalString(WSApiFlag.Name))
}

if ctx.GlobalIsSet(WSPathPrefixFlag.Name) {
cfg.WSPathPrefix = ctx.GlobalString(WSPathPrefixFlag.Name)
}
}

// setIPC creates an IPC path configuration from the set command line flags,
Expand Down
5 changes: 0 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,6 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down Expand Up @@ -501,7 +500,6 @@ golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapK
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
Expand Down Expand Up @@ -549,15 +547,12 @@ google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyz
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
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=
Expand Down
14 changes: 3 additions & 11 deletions graphql/graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import (
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"

"github.com/stretchr/testify/assert"
)

func TestBuildSchema(t *testing.T) {
Expand Down Expand Up @@ -166,18 +168,8 @@ func TestGraphQLHTTPOnSamePort_GQLRequest_Unsuccessful(t *testing.T) {
if err != nil {
t.Fatalf("could not post: %v", err)
}
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("could not read from response body: %v", err)
}
resp.Body.Close()
// make sure the request is not handled successfully
if want, have := "404 page not found\n", string(bodyBytes); have != want {
t.Errorf("have:\n%v\nwant:\n%v", have, want)
}
if want, have := 404, resp.StatusCode; want != have {
t.Errorf("wrong statuscode, have:\n%v\nwant:%v", have, want)
}
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
}

func createNode(t *testing.T, gqlEnabled bool) *node.Node {
Expand Down
3 changes: 3 additions & 0 deletions node/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,10 @@ func TestStartRPC(t *testing.T) {
}

for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()

// Apply some sane defaults.
config := test.cfg
// config.Logger = testlog.Logger(t, log.LvlDebug)
Expand Down
6 changes: 6 additions & 0 deletions node/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ type Config struct {
// interface.
HTTPTimeouts rpc.HTTPTimeouts

// HTTPPathPrefix specifies a path prefix on which http-rpc is to be served.
HTTPPathPrefix string `toml:",omitempty"`

// WSHost is the host interface on which to start the websocket RPC server. If
// this field is empty, no websocket API endpoint will be started.
WSHost string
Expand All @@ -148,6 +151,9 @@ type Config struct {
// ephemeral nodes).
WSPort int `toml:",omitempty"`

// WSPathPrefix specifies a path prefix on which ws-rpc is to be served.
WSPathPrefix string `toml:",omitempty"`

// WSOrigins is the list of domain to accept websocket requests from. Please be
// aware that the server can only act upon the HTTP request the client sends and
// cannot verify the validity of the request header.
Expand Down
20 changes: 16 additions & 4 deletions node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,14 @@ func New(conf *Config) (*Node, error) {
node.server.Config.NodeDatabase = node.config.NodeDB()
}

// Check HTTP/WS prefixes are valid.
if err := validatePrefix("HTTP", conf.HTTPPathPrefix); err != nil {
return nil, err
}
if err := validatePrefix("WebSocket", conf.WSPathPrefix); err != nil {
return nil, err
}

// Configure RPC servers.
node.http = newHTTPServer(node.log, conf.HTTPTimeouts)
node.ws = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts)
Expand Down Expand Up @@ -346,6 +354,7 @@ func (n *Node) startRPC() error {
CorsAllowedOrigins: n.config.HTTPCors,
Vhosts: n.config.HTTPVirtualHosts,
Modules: n.config.HTTPModules,
prefix: n.config.HTTPPathPrefix,
}
if err := n.http.setListenAddr(n.config.HTTPHost, n.config.HTTPPort); err != nil {
return err
Expand All @@ -361,6 +370,7 @@ func (n *Node) startRPC() error {
config := wsConfig{
Modules: n.config.WSModules,
Origins: n.config.WSOrigins,
prefix: n.config.WSPathPrefix,
}
if err := server.setListenAddr(n.config.WSHost, n.config.WSPort); err != nil {
return err
Expand Down Expand Up @@ -457,6 +467,7 @@ func (n *Node) RegisterHandler(name, path string, handler http.Handler) {
if n.state != initializingState {
panic("can't register HTTP handler on running/stopped node")
}

n.http.mux.Handle(path, handler)
n.http.handlerNames[path] = name
}
Expand Down Expand Up @@ -513,17 +524,18 @@ func (n *Node) IPCEndpoint() string {
return n.ipc.endpoint
}

// HTTPEndpoint returns the URL of the HTTP server.
// HTTPEndpoint returns the URL of the HTTP server. Note that this URL does not
// contain the JSON-RPC path prefix set by HTTPPathPrefix.
func (n *Node) HTTPEndpoint() string {
return "http://" + n.http.listenAddr()
}

// WSEndpoint retrieves the current WS endpoint used by the protocol stack.
// WSEndpoint returns the current JSON-RPC over WebSocket endpoint.
func (n *Node) WSEndpoint() string {
if n.http.wsAllowed() {
return "ws://" + n.http.listenAddr()
return "ws://" + n.http.listenAddr() + n.http.wsConfig.prefix
}
return "ws://" + n.ws.listenAddr()
return "ws://" + n.ws.listenAddr() + n.ws.wsConfig.prefix
}

// EventMux retrieves the event multiplexer used by all the network services in
Expand Down
107 changes: 106 additions & 1 deletion node/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ func TestLifecycleTerminationGuarantee(t *testing.T) {
}

// Tests whether a handler can be successfully mounted on the canonical HTTP server
// on the given path
// on the given prefix
func TestRegisterHandler_Successful(t *testing.T) {
node := createNode(t, 7878, 7979)

Expand Down Expand Up @@ -483,7 +483,112 @@ func TestWebsocketHTTPOnSeparatePort_WSRequest(t *testing.T) {
if !checkRPC(node.HTTPEndpoint()) {
t.Fatalf("http request failed")
}
}

type rpcPrefixTest struct {
httpPrefix, wsPrefix string
// These lists paths on which JSON-RPC should be served / not served.
wantHTTP []string
wantNoHTTP []string
wantWS []string
wantNoWS []string
}

func TestNodeRPCPrefix(t *testing.T) {
t.Parallel()

tests := []rpcPrefixTest{
// both off
{
httpPrefix: "", wsPrefix: "",
wantHTTP: []string{"/", "/?p=1"},
wantNoHTTP: []string{"/test", "/test?p=1"},
wantWS: []string{"/", "/?p=1"},
wantNoWS: []string{"/test", "/test?p=1"},
},
// only http prefix
{
httpPrefix: "/testprefix", wsPrefix: "",
wantHTTP: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
wantNoHTTP: []string{"/", "/?p=1", "/test", "/test?p=1"},
wantWS: []string{"/", "/?p=1"},
wantNoWS: []string{"/testprefix", "/testprefix?p=1", "/test", "/test?p=1"},
},
// only ws prefix
{
httpPrefix: "", wsPrefix: "/testprefix",
wantHTTP: []string{"/", "/?p=1"},
wantNoHTTP: []string{"/testprefix", "/testprefix?p=1", "/test", "/test?p=1"},
wantWS: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
wantNoWS: []string{"/", "/?p=1", "/test", "/test?p=1"},
},
// both set
{
httpPrefix: "/testprefix", wsPrefix: "/testprefix",
wantHTTP: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
wantNoHTTP: []string{"/", "/?p=1", "/test", "/test?p=1"},
wantWS: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
wantNoWS: []string{"/", "/?p=1", "/test", "/test?p=1"},
},
}

for _, test := range tests {
test := test
name := fmt.Sprintf("http=%s ws=%s", test.httpPrefix, test.wsPrefix)
t.Run(name, func(t *testing.T) {
cfg := &Config{
HTTPHost: "127.0.0.1",
HTTPPathPrefix: test.httpPrefix,
WSHost: "127.0.0.1",
WSPathPrefix: test.wsPrefix,
}
node, err := New(cfg)
if err != nil {
t.Fatal("can't create node:", err)
}
defer node.Close()
if err := node.Start(); err != nil {
t.Fatal("can't start node:", err)
}
test.check(t, node)
})
}
}

func (test rpcPrefixTest) check(t *testing.T, node *Node) {
t.Helper()
httpBase := "http://" + node.http.listenAddr()
wsBase := "ws://" + node.http.listenAddr()

if node.WSEndpoint() != wsBase+test.wsPrefix {
t.Errorf("Error: node has wrong WSEndpoint %q", node.WSEndpoint())
}

for _, path := range test.wantHTTP {
resp := rpcRequest(t, httpBase+path)
if resp.StatusCode != 200 {
t.Errorf("Error: %s: bad status code %d, want 200", path, resp.StatusCode)
}
}
for _, path := range test.wantNoHTTP {
resp := rpcRequest(t, httpBase+path)
if resp.StatusCode != 404 {
t.Errorf("Error: %s: bad status code %d, want 404", path, resp.StatusCode)
}
}
for _, path := range test.wantWS {
err := wsRequest(t, wsBase+path, "")
if err != nil {
t.Errorf("Error: %s: WebSocket connection failed: %v", path, err)
}
}
for _, path := range test.wantNoWS {
err := wsRequest(t, wsBase+path, "")
if err == nil {
t.Errorf("Error: %s: WebSocket connection succeeded for path in wantNoWS", path)
}

}
}

func createNode(t *testing.T, httpPort, wsPort int) *Node {
Expand Down
Loading

0 comments on commit 4eae0c6

Please sign in to comment.