Skip to content

node: serve JSON-RPC on custom path prefix #22184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Feb 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ var (
utils.GraphQLCORSDomainFlag,
utils.GraphQLVirtualHostsFlag,
utils.HTTPApiFlag,
utils.HTTPPathPrefixFlag,
utils.LegacyRPCApiFlag,
utils.WSEnabledFlag,
utils.WSListenAddrFlag,
Expand All @@ -188,6 +189,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 @@ -137,12 +137,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 @@ -532,6 +532,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 @@ -570,6 +575,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 @@ -955,6 +965,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 @@ -1004,6 +1018,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 @@ -435,7 +435,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 @@ -503,7 +502,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 @@ -551,15 +549,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