Skip to content

Commit 4eae0c6

Browse files
renaynayfjl
andauthored
cmd/geth, node: allow configuring JSON-RPC on custom path prefix (ethereum#22184)
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 ethereum#21826 Co-authored-by: Felix Lange <[email protected]>
1 parent e3430ac commit 4eae0c6

11 files changed

+320
-65
lines changed

cmd/geth/main.go

+2
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ var (
181181
utils.GraphQLCORSDomainFlag,
182182
utils.GraphQLVirtualHostsFlag,
183183
utils.HTTPApiFlag,
184+
utils.HTTPPathPrefixFlag,
184185
utils.LegacyRPCApiFlag,
185186
utils.WSEnabledFlag,
186187
utils.WSListenAddrFlag,
@@ -190,6 +191,7 @@ var (
190191
utils.WSApiFlag,
191192
utils.LegacyWSApiFlag,
192193
utils.WSAllowedOriginsFlag,
194+
utils.WSPathPrefixFlag,
193195
utils.LegacyWSAllowedOriginsFlag,
194196
utils.IPCDisabledFlag,
195197
utils.IPCPathFlag,

cmd/geth/usage.go

+2
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,14 @@ var AppHelpFlagGroups = []flags.FlagGroup{
138138
utils.HTTPListenAddrFlag,
139139
utils.HTTPPortFlag,
140140
utils.HTTPApiFlag,
141+
utils.HTTPPathPrefixFlag,
141142
utils.HTTPCORSDomainFlag,
142143
utils.HTTPVirtualHostsFlag,
143144
utils.WSEnabledFlag,
144145
utils.WSListenAddrFlag,
145146
utils.WSPortFlag,
146147
utils.WSApiFlag,
148+
utils.WSPathPrefixFlag,
147149
utils.WSAllowedOriginsFlag,
148150
utils.GraphQLEnabledFlag,
149151
utils.GraphQLCORSDomainFlag,

cmd/utils/flags.go

+18
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,11 @@ var (
531531
Usage: "API's offered over the HTTP-RPC interface",
532532
Value: "",
533533
}
534+
HTTPPathPrefixFlag = cli.StringFlag{
535+
Name: "http.rpcprefix",
536+
Usage: "HTTP path path prefix on which JSON-RPC is served. Use '/' to serve on all paths.",
537+
Value: "",
538+
}
534539
GraphQLEnabledFlag = cli.BoolFlag{
535540
Name: "graphql",
536541
Usage: "Enable GraphQL on the HTTP-RPC server. Note that GraphQL can only be started if an HTTP server is started as well.",
@@ -569,6 +574,11 @@ var (
569574
Usage: "Origins from which to accept websockets requests",
570575
Value: "",
571576
}
577+
WSPathPrefixFlag = cli.StringFlag{
578+
Name: "ws.rpcprefix",
579+
Usage: "HTTP path prefix on which JSON-RPC is served. Use '/' to serve on all paths.",
580+
Value: "",
581+
}
572582
ExecFlag = cli.StringFlag{
573583
Name: "exec",
574584
Usage: "Execute JavaScript statement",
@@ -946,6 +956,10 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) {
946956
if ctx.GlobalIsSet(HTTPVirtualHostsFlag.Name) {
947957
cfg.HTTPVirtualHosts = SplitAndTrim(ctx.GlobalString(HTTPVirtualHostsFlag.Name))
948958
}
959+
960+
if ctx.GlobalIsSet(HTTPPathPrefixFlag.Name) {
961+
cfg.HTTPPathPrefix = ctx.GlobalString(HTTPPathPrefixFlag.Name)
962+
}
949963
}
950964

951965
// setGraphQL creates the GraphQL listener interface string from the set
@@ -995,6 +1009,10 @@ func setWS(ctx *cli.Context, cfg *node.Config) {
9951009
if ctx.GlobalIsSet(WSApiFlag.Name) {
9961010
cfg.WSModules = SplitAndTrim(ctx.GlobalString(WSApiFlag.Name))
9971011
}
1012+
1013+
if ctx.GlobalIsSet(WSPathPrefixFlag.Name) {
1014+
cfg.WSPathPrefix = ctx.GlobalString(WSPathPrefixFlag.Name)
1015+
}
9981016
}
9991017

10001018
// setIPC creates an IPC path configuration from the set command line flags,

go.sum

-5
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,6 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
433433
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
434434
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
435435
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
436-
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
437436
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
438437
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
439438
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -501,7 +500,6 @@ golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapK
501500
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
502501
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
503502
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
504-
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
505503
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
506504
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
507505
gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
@@ -549,15 +547,12 @@ google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyz
549547
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
550548
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
551549
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
552-
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
553550
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
554551
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
555552
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
556-
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
557553
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
558554
gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6 h1:a6cXbcDDUkSBlpnkWV1bJ+vv3mOgQEltEJ2rPxroVu0=
559555
gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns=
560-
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
561556
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
562557
gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0=
563558
gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0=

graphql/graphql_test.go

+3-11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import (
3030
"github.com/ethereum/go-ethereum/eth"
3131
"github.com/ethereum/go-ethereum/node"
3232
"github.com/ethereum/go-ethereum/params"
33+
34+
"github.com/stretchr/testify/assert"
3335
)
3436

3537
func TestBuildSchema(t *testing.T) {
@@ -166,18 +168,8 @@ func TestGraphQLHTTPOnSamePort_GQLRequest_Unsuccessful(t *testing.T) {
166168
if err != nil {
167169
t.Fatalf("could not post: %v", err)
168170
}
169-
bodyBytes, err := ioutil.ReadAll(resp.Body)
170-
if err != nil {
171-
t.Fatalf("could not read from response body: %v", err)
172-
}
173-
resp.Body.Close()
174171
// make sure the request is not handled successfully
175-
if want, have := "404 page not found\n", string(bodyBytes); have != want {
176-
t.Errorf("have:\n%v\nwant:\n%v", have, want)
177-
}
178-
if want, have := 404, resp.StatusCode; want != have {
179-
t.Errorf("wrong statuscode, have:\n%v\nwant:%v", have, want)
180-
}
172+
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
181173
}
182174

183175
func createNode(t *testing.T, gqlEnabled bool) *node.Node {

node/api_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,10 @@ func TestStartRPC(t *testing.T) {
244244
}
245245

246246
for _, test := range tests {
247+
test := test
247248
t.Run(test.name, func(t *testing.T) {
249+
t.Parallel()
250+
248251
// Apply some sane defaults.
249252
config := test.cfg
250253
// config.Logger = testlog.Logger(t, log.LvlDebug)

node/config.go

+6
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ type Config struct {
139139
// interface.
140140
HTTPTimeouts rpc.HTTPTimeouts
141141

142+
// HTTPPathPrefix specifies a path prefix on which http-rpc is to be served.
143+
HTTPPathPrefix string `toml:",omitempty"`
144+
142145
// WSHost is the host interface on which to start the websocket RPC server. If
143146
// this field is empty, no websocket API endpoint will be started.
144147
WSHost string
@@ -148,6 +151,9 @@ type Config struct {
148151
// ephemeral nodes).
149152
WSPort int `toml:",omitempty"`
150153

154+
// WSPathPrefix specifies a path prefix on which ws-rpc is to be served.
155+
WSPathPrefix string `toml:",omitempty"`
156+
151157
// WSOrigins is the list of domain to accept websocket requests from. Please be
152158
// aware that the server can only act upon the HTTP request the client sends and
153159
// cannot verify the validity of the request header.

node/node.go

+16-4
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ func New(conf *Config) (*Node, error) {
135135
node.server.Config.NodeDatabase = node.config.NodeDB()
136136
}
137137

138+
// Check HTTP/WS prefixes are valid.
139+
if err := validatePrefix("HTTP", conf.HTTPPathPrefix); err != nil {
140+
return nil, err
141+
}
142+
if err := validatePrefix("WebSocket", conf.WSPathPrefix); err != nil {
143+
return nil, err
144+
}
145+
138146
// Configure RPC servers.
139147
node.http = newHTTPServer(node.log, conf.HTTPTimeouts)
140148
node.ws = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts)
@@ -346,6 +354,7 @@ func (n *Node) startRPC() error {
346354
CorsAllowedOrigins: n.config.HTTPCors,
347355
Vhosts: n.config.HTTPVirtualHosts,
348356
Modules: n.config.HTTPModules,
357+
prefix: n.config.HTTPPathPrefix,
349358
}
350359
if err := n.http.setListenAddr(n.config.HTTPHost, n.config.HTTPPort); err != nil {
351360
return err
@@ -361,6 +370,7 @@ func (n *Node) startRPC() error {
361370
config := wsConfig{
362371
Modules: n.config.WSModules,
363372
Origins: n.config.WSOrigins,
373+
prefix: n.config.WSPathPrefix,
364374
}
365375
if err := server.setListenAddr(n.config.WSHost, n.config.WSPort); err != nil {
366376
return err
@@ -457,6 +467,7 @@ func (n *Node) RegisterHandler(name, path string, handler http.Handler) {
457467
if n.state != initializingState {
458468
panic("can't register HTTP handler on running/stopped node")
459469
}
470+
460471
n.http.mux.Handle(path, handler)
461472
n.http.handlerNames[path] = name
462473
}
@@ -513,17 +524,18 @@ func (n *Node) IPCEndpoint() string {
513524
return n.ipc.endpoint
514525
}
515526

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

521-
// WSEndpoint retrieves the current WS endpoint used by the protocol stack.
533+
// WSEndpoint returns the current JSON-RPC over WebSocket endpoint.
522534
func (n *Node) WSEndpoint() string {
523535
if n.http.wsAllowed() {
524-
return "ws://" + n.http.listenAddr()
536+
return "ws://" + n.http.listenAddr() + n.http.wsConfig.prefix
525537
}
526-
return "ws://" + n.ws.listenAddr()
538+
return "ws://" + n.ws.listenAddr() + n.ws.wsConfig.prefix
527539
}
528540

529541
// EventMux retrieves the event multiplexer used by all the network services in

node/node_test.go

+106-1
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ func TestLifecycleTerminationGuarantee(t *testing.T) {
390390
}
391391

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

@@ -483,7 +483,112 @@ func TestWebsocketHTTPOnSeparatePort_WSRequest(t *testing.T) {
483483
if !checkRPC(node.HTTPEndpoint()) {
484484
t.Fatalf("http request failed")
485485
}
486+
}
487+
488+
type rpcPrefixTest struct {
489+
httpPrefix, wsPrefix string
490+
// These lists paths on which JSON-RPC should be served / not served.
491+
wantHTTP []string
492+
wantNoHTTP []string
493+
wantWS []string
494+
wantNoWS []string
495+
}
496+
497+
func TestNodeRPCPrefix(t *testing.T) {
498+
t.Parallel()
499+
500+
tests := []rpcPrefixTest{
501+
// both off
502+
{
503+
httpPrefix: "", wsPrefix: "",
504+
wantHTTP: []string{"/", "/?p=1"},
505+
wantNoHTTP: []string{"/test", "/test?p=1"},
506+
wantWS: []string{"/", "/?p=1"},
507+
wantNoWS: []string{"/test", "/test?p=1"},
508+
},
509+
// only http prefix
510+
{
511+
httpPrefix: "/testprefix", wsPrefix: "",
512+
wantHTTP: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
513+
wantNoHTTP: []string{"/", "/?p=1", "/test", "/test?p=1"},
514+
wantWS: []string{"/", "/?p=1"},
515+
wantNoWS: []string{"/testprefix", "/testprefix?p=1", "/test", "/test?p=1"},
516+
},
517+
// only ws prefix
518+
{
519+
httpPrefix: "", wsPrefix: "/testprefix",
520+
wantHTTP: []string{"/", "/?p=1"},
521+
wantNoHTTP: []string{"/testprefix", "/testprefix?p=1", "/test", "/test?p=1"},
522+
wantWS: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
523+
wantNoWS: []string{"/", "/?p=1", "/test", "/test?p=1"},
524+
},
525+
// both set
526+
{
527+
httpPrefix: "/testprefix", wsPrefix: "/testprefix",
528+
wantHTTP: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
529+
wantNoHTTP: []string{"/", "/?p=1", "/test", "/test?p=1"},
530+
wantWS: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
531+
wantNoWS: []string{"/", "/?p=1", "/test", "/test?p=1"},
532+
},
533+
}
534+
535+
for _, test := range tests {
536+
test := test
537+
name := fmt.Sprintf("http=%s ws=%s", test.httpPrefix, test.wsPrefix)
538+
t.Run(name, func(t *testing.T) {
539+
cfg := &Config{
540+
HTTPHost: "127.0.0.1",
541+
HTTPPathPrefix: test.httpPrefix,
542+
WSHost: "127.0.0.1",
543+
WSPathPrefix: test.wsPrefix,
544+
}
545+
node, err := New(cfg)
546+
if err != nil {
547+
t.Fatal("can't create node:", err)
548+
}
549+
defer node.Close()
550+
if err := node.Start(); err != nil {
551+
t.Fatal("can't start node:", err)
552+
}
553+
test.check(t, node)
554+
})
555+
}
556+
}
557+
558+
func (test rpcPrefixTest) check(t *testing.T, node *Node) {
559+
t.Helper()
560+
httpBase := "http://" + node.http.listenAddr()
561+
wsBase := "ws://" + node.http.listenAddr()
562+
563+
if node.WSEndpoint() != wsBase+test.wsPrefix {
564+
t.Errorf("Error: node has wrong WSEndpoint %q", node.WSEndpoint())
565+
}
566+
567+
for _, path := range test.wantHTTP {
568+
resp := rpcRequest(t, httpBase+path)
569+
if resp.StatusCode != 200 {
570+
t.Errorf("Error: %s: bad status code %d, want 200", path, resp.StatusCode)
571+
}
572+
}
573+
for _, path := range test.wantNoHTTP {
574+
resp := rpcRequest(t, httpBase+path)
575+
if resp.StatusCode != 404 {
576+
t.Errorf("Error: %s: bad status code %d, want 404", path, resp.StatusCode)
577+
}
578+
}
579+
for _, path := range test.wantWS {
580+
err := wsRequest(t, wsBase+path, "")
581+
if err != nil {
582+
t.Errorf("Error: %s: WebSocket connection failed: %v", path, err)
583+
}
584+
}
585+
for _, path := range test.wantNoWS {
586+
err := wsRequest(t, wsBase+path, "")
587+
if err == nil {
588+
t.Errorf("Error: %s: WebSocket connection succeeded for path in wantNoWS", path)
589+
}
486590

591+
}
487592
}
488593

489594
func createNode(t *testing.T, httpPort, wsPort int) *Node {

0 commit comments

Comments
 (0)