Skip to content

Commit 64f8767

Browse files
JukLee0iragzliudan
authored andcommitted
node: serve JSON-RPC on custom path prefix ethereum#22184 (XinFinOrg#952)
1 parent 371bad1 commit 64f8767

File tree

8 files changed

+410
-52
lines changed

8 files changed

+410
-52
lines changed

cmd/XDC/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,13 @@ var (
149149
utils.HTTPWriteTimeoutFlag,
150150
utils.HTTPIdleTimeoutFlag,
151151
utils.HTTPApiFlag,
152+
utils.HTTPPathPrefixFlag,
152153
utils.WSEnabledFlag,
153154
utils.WSListenAddrFlag,
154155
utils.WSPortFlag,
155156
utils.WSApiFlag,
156157
utils.WSAllowedOriginsFlag,
158+
utils.WSPathPrefixFlag,
157159
utils.IPCDisabledFlag,
158160
utils.IPCPathFlag,
159161
utils.RPCGlobalTxFeeCap,

cmd/utils/flags.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,12 @@ var (
463463
Value: "debug,eth,net,personal,txpool,web3,XDPoS",
464464
Category: flags.APICategory,
465465
}
466+
HTTPPathPrefixFlag = &cli.StringFlag{
467+
Name: "http-rpcprefix",
468+
Usage: "HTTP path path prefix on which JSON-RPC is served. Use '/' to serve on all paths.",
469+
Value: "",
470+
Category: flags.APICategory,
471+
}
466472
HTTPReadTimeoutFlag = &cli.DurationFlag{
467473
Name: "http-readtimeout",
468474
Aliases: []string{"rpcreadtimeout"},
@@ -518,6 +524,12 @@ var (
518524
Value: "*",
519525
Category: flags.APICategory,
520526
}
527+
WSPathPrefixFlag = &cli.StringFlag{
528+
Name: "ws-rpcprefix",
529+
Usage: "HTTP path prefix on which JSON-RPC is served. Use '/' to serve on all paths.",
530+
Value: "",
531+
Category: flags.APICategory,
532+
}
521533
ExecFlag = &cli.StringFlag{
522534
Name: "exec",
523535
Usage: "Execute JavaScript statement",
@@ -982,6 +994,9 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) {
982994
if ctx.IsSet(HTTPPortFlag.Name) {
983995
cfg.HTTPPort = ctx.Int(HTTPPortFlag.Name)
984996
}
997+
if ctx.IsSet(HTTPPathPrefixFlag.Name) {
998+
cfg.HTTPPathPrefix = ctx.String(HTTPPathPrefixFlag.Name)
999+
}
9851000
if ctx.IsSet(HTTPReadTimeoutFlag.Name) {
9861001
cfg.HTTPTimeouts.ReadTimeout = ctx.Duration(HTTPReadTimeoutFlag.Name)
9871002
}
@@ -1009,6 +1024,9 @@ func setWS(ctx *cli.Context, cfg *node.Config) {
10091024
if ctx.IsSet(WSPortFlag.Name) {
10101025
cfg.WSPort = ctx.Int(WSPortFlag.Name)
10111026
}
1027+
if ctx.IsSet(WSPathPrefixFlag.Name) {
1028+
cfg.WSPathPrefix = ctx.String(WSPathPrefixFlag.Name)
1029+
}
10121030
cfg.WSOrigins = SplitAndTrim(ctx.String(WSAllowedOriginsFlag.Name))
10131031
cfg.WSModules = SplitAndTrim(ctx.String(WSApiFlag.Name))
10141032
}

node/api_test.go

Lines changed: 3 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ type Config struct {
131131
// interface.
132132
HTTPTimeouts rpc.HTTPTimeouts
133133

134+
// HTTPPathPrefix specifies a path prefix on which http-rpc is to be served.
135+
HTTPPathPrefix string `toml:",omitempty"`
136+
134137
// WSHost is the host interface on which to start the websocket RPC server. If
135138
// this field is empty, no websocket API endpoint will be started.
136139
WSHost string
@@ -140,6 +143,9 @@ type Config struct {
140143
// ephemeral nodes).
141144
WSPort int `toml:",omitempty"`
142145

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

node/node.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ func New(conf *Config) (*Node, error) {
141141
node.server.Config.NodeDatabase = node.config.NodeDB()
142142
}
143143

144+
// Check HTTP/WS prefixes are valid.
145+
if err := validatePrefix("HTTP", conf.HTTPPathPrefix); err != nil {
146+
return nil, err
147+
}
148+
if err := validatePrefix("WebSocket", conf.WSPathPrefix); err != nil {
149+
return nil, err
150+
}
151+
144152
//make sure timeout values are meaningful
145153
CheckTimeouts(&conf.HTTPTimeouts)
146154
// Configure RPC servers.
@@ -357,6 +365,7 @@ func (n *Node) startRPC() error {
357365
CorsAllowedOrigins: n.config.HTTPCors,
358366
Vhosts: n.config.HTTPVirtualHosts,
359367
Modules: n.config.HTTPModules,
368+
prefix: n.config.HTTPPathPrefix,
360369
}
361370
if err := n.http.setListenAddr(n.config.HTTPHost, n.config.HTTPPort); err != nil {
362371
return err
@@ -372,6 +381,7 @@ func (n *Node) startRPC() error {
372381
config := wsConfig{
373382
Modules: n.config.WSModules,
374383
Origins: n.config.WSOrigins,
384+
prefix: n.config.WSPathPrefix,
375385
}
376386
if err := server.setListenAddr(n.config.WSHost, n.config.WSPort); err != nil {
377387
return err
@@ -529,17 +539,18 @@ func (n *Node) IPCEndpoint() string {
529539
return n.ipc.endpoint
530540
}
531541

532-
// HTTPEndpoint retrieves the current HTTP endpoint used by the protocol stack.
542+
// HTTPEndpoint returns the URL of the HTTP server. Note that this URL does not
543+
// contain the JSON-RPC path prefix set by HTTPPathPrefix.
533544
func (n *Node) HTTPEndpoint() string {
534545
return "http://" + n.http.listenAddr()
535546
}
536547

537-
// WSEndpoint retrieves the current WS endpoint used by the protocol stack.
548+
// WSEndpoint returns the current JSON-RPC over WebSocket endpoint.
538549
func (n *Node) WSEndpoint() string {
539550
if n.http.wsAllowed() {
540-
return "ws://" + n.http.listenAddr()
551+
return "ws://" + n.http.listenAddr() + n.http.wsConfig.prefix
541552
}
542-
return "ws://" + n.ws.listenAddr()
553+
return "ws://" + n.ws.listenAddr() + n.ws.wsConfig.prefix
543554
}
544555

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

node/node_test.go

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ func TestLifecycleTerminationGuarantee(t *testing.T) {
363363
}
364364

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

@@ -427,6 +427,112 @@ func TestWebsocketHTTPOnSamePort_WebsocketRequest(t *testing.T) {
427427
}
428428
}
429429

430+
type rpcPrefixTest struct {
431+
httpPrefix, wsPrefix string
432+
// These lists paths on which JSON-RPC should be served / not served.
433+
wantHTTP []string
434+
wantNoHTTP []string
435+
wantWS []string
436+
wantNoWS []string
437+
}
438+
439+
func TestNodeRPCPrefix(t *testing.T) {
440+
t.Parallel()
441+
442+
tests := []rpcPrefixTest{
443+
// both off
444+
{
445+
httpPrefix: "", wsPrefix: "",
446+
wantHTTP: []string{"/", "/?p=1"},
447+
wantNoHTTP: []string{"/test", "/test?p=1"},
448+
wantWS: []string{"/", "/?p=1"},
449+
wantNoWS: []string{"/test", "/test?p=1"},
450+
},
451+
// only http prefix
452+
{
453+
httpPrefix: "/testprefix", wsPrefix: "",
454+
wantHTTP: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
455+
wantNoHTTP: []string{"/", "/?p=1", "/test", "/test?p=1"},
456+
wantWS: []string{"/", "/?p=1"},
457+
wantNoWS: []string{"/testprefix", "/testprefix?p=1", "/test", "/test?p=1"},
458+
},
459+
// only ws prefix
460+
{
461+
httpPrefix: "", wsPrefix: "/testprefix",
462+
wantHTTP: []string{"/", "/?p=1"},
463+
wantNoHTTP: []string{"/testprefix", "/testprefix?p=1", "/test", "/test?p=1"},
464+
wantWS: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
465+
wantNoWS: []string{"/", "/?p=1", "/test", "/test?p=1"},
466+
},
467+
// both set
468+
{
469+
httpPrefix: "/testprefix", wsPrefix: "/testprefix",
470+
wantHTTP: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
471+
wantNoHTTP: []string{"/", "/?p=1", "/test", "/test?p=1"},
472+
wantWS: []string{"/testprefix", "/testprefix?p=1", "/testprefix/x", "/testprefix/x?p=1"},
473+
wantNoWS: []string{"/", "/?p=1", "/test", "/test?p=1"},
474+
},
475+
}
476+
477+
for _, test := range tests {
478+
test := test
479+
name := fmt.Sprintf("http=%s ws=%s", test.httpPrefix, test.wsPrefix)
480+
t.Run(name, func(t *testing.T) {
481+
cfg := &Config{
482+
HTTPHost: "127.0.0.1",
483+
HTTPPathPrefix: test.httpPrefix,
484+
WSHost: "127.0.0.1",
485+
WSPathPrefix: test.wsPrefix,
486+
}
487+
node, err := New(cfg)
488+
if err != nil {
489+
t.Fatal("can't create node:", err)
490+
}
491+
defer node.Close()
492+
if err := node.Start(); err != nil {
493+
t.Fatal("can't start node:", err)
494+
}
495+
test.check(t, node)
496+
})
497+
}
498+
}
499+
500+
func (test rpcPrefixTest) check(t *testing.T, node *Node) {
501+
t.Helper()
502+
httpBase := "http://" + node.http.listenAddr()
503+
wsBase := "ws://" + node.http.listenAddr()
504+
505+
if node.WSEndpoint() != wsBase+test.wsPrefix {
506+
t.Errorf("Error: node has wrong WSEndpoint %q", node.WSEndpoint())
507+
}
508+
509+
for _, path := range test.wantHTTP {
510+
resp := rpcRequest(t, httpBase+path)
511+
if resp.StatusCode != 200 {
512+
t.Errorf("Error: %s: bad status code %d, want 200", path, resp.StatusCode)
513+
}
514+
}
515+
for _, path := range test.wantNoHTTP {
516+
resp := rpcRequest(t, httpBase+path)
517+
if resp.StatusCode != 404 {
518+
t.Errorf("Error: %s: bad status code %d, want 404", path, resp.StatusCode)
519+
}
520+
}
521+
for _, path := range test.wantWS {
522+
err := wsRequest(t, wsBase+path, "")
523+
if err != nil {
524+
t.Errorf("Error: %s: WebSocket connection failed: %v", path, err)
525+
}
526+
}
527+
for _, path := range test.wantNoWS {
528+
err := wsRequest(t, wsBase+path, "")
529+
if err == nil {
530+
t.Errorf("Error: %s: WebSocket connection succeeded for path in wantNoWS", path)
531+
}
532+
533+
}
534+
}
535+
430536
func TestWebsocketHTTPOnSeparatePort_WSRequest(t *testing.T) {
431537
// try and get a free port
432538
listener, err := net.Listen("tcp", "127.0.0.1:0")

node/rpcstack.go

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,14 @@ type httpConfig struct {
3939
Modules []string
4040
CorsAllowedOrigins []string
4141
Vhosts []string
42+
prefix string // path prefix on which to mount http handler
4243
}
4344

4445
// wsConfig is the JSON-RPC/Websocket configuration
4546
type wsConfig struct {
4647
Origins []string
4748
Modules []string
49+
prefix string // path prefix on which to mount ws handler
4850
}
4951

5052
type rpcHandler struct {
@@ -141,12 +143,18 @@ func (h *httpServer) start() error {
141143

142144
// if server is websocket only, return after logging
143145
if h.wsAllowed() && !h.rpcAllowed() {
146+
url := fmt.Sprintf("ws://%v", listener.Addr())
147+
if h.wsConfig.prefix != "" {
148+
url += h.wsConfig.prefix
149+
}
150+
h.log.Info("WebSocket enabled", "url", url)
144151
h.log.Info("WebSocket enabled", "url", fmt.Sprintf("ws://%v", listener.Addr()))
145152
return nil
146153
}
147154
// Log http endpoint.
148155
h.log.Info("HTTP server started",
149156
"endpoint", listener.Addr(),
157+
"prefix", h.httpConfig.prefix,
150158
"cors", strings.Join(h.httpConfig.CorsAllowedOrigins, ","),
151159
"vhosts", strings.Join(h.httpConfig.Vhosts, ","),
152160
)
@@ -169,26 +177,60 @@ func (h *httpServer) start() error {
169177
}
170178

171179
func (h *httpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
172-
rpc := h.httpHandler.Load().(*rpcHandler)
173-
if r.RequestURI == "/" {
174-
// Serve JSON-RPC on the root path.
175-
ws := h.wsHandler.Load().(*rpcHandler)
176-
if ws != nil && isWebsocket(r) {
180+
// check if ws request and serve if ws enabled
181+
ws := h.wsHandler.Load().(*rpcHandler)
182+
if ws != nil && isWebsocket(r) {
183+
if checkPath(r, h.wsConfig.prefix) {
177184
ws.ServeHTTP(w, r)
185+
}
186+
return
187+
}
188+
// if http-rpc is enabled, try to serve request
189+
rpc := h.httpHandler.Load().(*rpcHandler)
190+
if rpc != nil {
191+
// First try to route in the mux.
192+
// Requests to a path below root are handled by the mux,
193+
// which has all the handlers registered via Node.RegisterHandler.
194+
// These are made available when RPC is enabled.
195+
muxHandler, pattern := h.mux.Handler(r)
196+
if pattern != "" {
197+
muxHandler.ServeHTTP(w, r)
178198
return
179199
}
180-
if rpc != nil {
200+
201+
if checkPath(r, h.httpConfig.prefix) {
181202
rpc.ServeHTTP(w, r)
182203
return
183204
}
184-
} else if rpc != nil {
185-
// Requests to a path below root are handled by the mux,
186-
// which has all the handlers registered via Node.RegisterHandler.
187-
// These are made available when RPC is enabled.
188-
h.mux.ServeHTTP(w, r)
189-
return
190205
}
191-
w.WriteHeader(404)
206+
w.WriteHeader(http.StatusNotFound)
207+
}
208+
209+
// checkPath checks whether a given request URL matches a given path prefix.
210+
func checkPath(r *http.Request, path string) bool {
211+
// if no prefix has been specified, request URL must be on root
212+
if path == "" {
213+
return r.URL.Path == "/"
214+
}
215+
// otherwise, check to make sure prefix matches
216+
return len(r.URL.Path) >= len(path) && r.URL.Path[:len(path)] == path
217+
}
218+
219+
// validatePrefix checks if 'path' is a valid configuration value for the RPC prefix option.
220+
func validatePrefix(what, path string) error {
221+
if path == "" {
222+
return nil
223+
}
224+
if path[0] != '/' {
225+
return fmt.Errorf(`%s RPC path prefix %q does not contain leading "/"`, what, path)
226+
}
227+
if strings.ContainsAny(path, "?#") {
228+
// This is just to avoid confusion. While these would match correctly (i.e. they'd
229+
// match if URL-escaped into path), it's not easy to understand for users when
230+
// setting that on the command line.
231+
return fmt.Errorf("%s RPC path prefix %q contains URL meta-characters", what, path)
232+
}
233+
return nil
192234
}
193235

194236
// stop shuts down the HTTP server.

0 commit comments

Comments
 (0)