From 75bd50a57e6fbcae89e8b515a1f308213280c74d Mon Sep 17 00:00:00 2001 From: Tolga Coplu Date: Fri, 21 Jul 2023 11:21:07 +0300 Subject: [PATCH] 145: Support rpc path prefix options --- rpc/http.go | 31 ++++++++++++++++++++++++++++--- rpc/node/config.go | 5 +++++ rpc/node/node.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/rpc/http.go b/rpc/http.go index 907d460..a381016 100644 --- a/rpc/http.go +++ b/rpc/http.go @@ -43,10 +43,12 @@ type HttpServer struct { // HttpConfig holds both http and websocket configuration elements type HttpConfig struct { HttpEndpoint string + HttpPathPrefix string HttpCors []string HttpCompress bool HttpTimeout time.Duration WsEndpoint string + WsPathPrefix string WsHandshakeTimeout time.Duration WsOnly bool } @@ -112,14 +114,26 @@ func (h *HttpServer) mainHandler(ctx *fasthttp.RequestCtx) { // The following code differentiates the cases based on the configurations and Config.WsOnly variable if strings.EqualFold(h.Config.HttpEndpoint, h.Config.WsEndpoint) { if isWebsocket(r) { - h.fastWsHandler(ctx) + if checkPath(r, h.Config.WsPathPrefix) { + h.fastWsHandler(ctx) + } else { + ctx.SetStatusCode(fasthttp.StatusNotFound) + } } else if !h.Config.WsOnly { - h.fastHTTPHandler(ctx, r) + if checkPath(r, h.Config.HttpPathPrefix) { + h.fastHTTPHandler(ctx, r) + } else { + ctx.SetStatusCode(fasthttp.StatusNotFound) + } } else { ctx.SetStatusCode(fasthttp.StatusNotFound) } } else { - h.fastHTTPHandler(ctx, r) + if checkPath(r, h.Config.HttpPathPrefix) { + h.fastHTTPHandler(ctx, r) + } else { + ctx.SetStatusCode(fasthttp.StatusNotFound) + } } } @@ -250,3 +264,14 @@ func isWebsocket(r *http.Request) bool { return strings.EqualFold(r.Header.Get("Upgrade"), "websocket") && strings.Contains(strings.ToLower(r.Header.Get("Connection")), "upgrade") } + +// checkPath checks whether a given request URL matches a given path prefix +func checkPath(r *http.Request, path string) bool { + if path == "*" { // if prefix is `*`, allow all paths + return true + } else if path == "" { // if no prefix has been specified, request URL must be on root + return r.URL.Path == "/" + } else { // otherwise, check to make sure prefix matches + return len(r.URL.Path) >= len(path) && r.URL.Path[:len(path)] == path + } +} diff --git a/rpc/node/config.go b/rpc/node/config.go index 626e6ef..c0d7d29 100644 --- a/rpc/node/config.go +++ b/rpc/node/config.go @@ -21,6 +21,7 @@ var ( defaultHttpTimeout time.Duration = 300 defaultWsHandshakeTimeout time.Duration = 10 defaultMaxBatchRequests uint = 1000 + DefaultPathPrefix string = "*" ) type Config struct { @@ -29,8 +30,10 @@ type Config struct { HttpCors []string `mapstructure:"httpCors"` HttpCompress bool `mapstructure:"httpCompress"` HttpTimeout time.Duration `mapstructure:"httpTimeout"` + HttpPathPrefix string `mapstructure:"httpPathPrefix"` WsPort int16 `mapstructure:"wsPort"` WsHost string `mapstructure:"wsHost"` + WsPathPrefix string `mapstructure:"wsPathPrefix"` WsHandshakeTimeout time.Duration `mapstructure:"wsHandshakeTimeout"` MaxBatchRequests uint `mapstructure:"maxBatchRequests"` } @@ -57,9 +60,11 @@ func defaultConfig() *Config { return &Config{ HttpPort: defaultHttpPort, HttpHost: defaultHttpHost, + HttpPathPrefix: DefaultPathPrefix, HttpCors: []string{}, HttpCompress: defaultHttpCompress, HttpTimeout: defaultHttpTimeout, + WsPathPrefix: DefaultPathPrefix, WsHandshakeTimeout: defaultWsHandshakeTimeout, MaxBatchRequests: defaultMaxBatchRequests, } diff --git a/rpc/node/node.go b/rpc/node/node.go index 0233769..9b26275 100644 --- a/rpc/node/node.go +++ b/rpc/node/node.go @@ -2,6 +2,7 @@ package node import ( "context" + "fmt" "strings" "github.com/aurora-is-near/relayer2-base/broker" @@ -26,12 +27,20 @@ func NewWithConf(config *Config) (*RpcNode, error) { // If httpEndpoint is not empty, then a HttpServer should be initialized (no matters if it is http only or http and ws) if config.httpEndpoint() != "" { + if err := validatePath(config.HttpPathPrefix); err != nil { + logger.Fatal().Err(err).Msg("HTTP config err:") + } else if err := validatePath(config.WsPathPrefix); err != nil { + logger.Fatal().Err(err).Msg("Websocket config err:") + } + httpCfg := rpc.HttpConfig{ HttpEndpoint: config.httpEndpoint(), + HttpPathPrefix: config.HttpPathPrefix, HttpCors: config.HttpCors, HttpCompress: config.HttpCompress, HttpTimeout: config.HttpTimeout, WsEndpoint: config.wsEndpoint(), + WsPathPrefix: config.WsPathPrefix, WsHandshakeTimeout: config.WsHandshakeTimeout, WsOnly: false, } @@ -41,12 +50,18 @@ func NewWithConf(config *Config) (*RpcNode, error) { // If wsEndpoint is not empty and different from httpEndpoint, then another HttpServer should be initialized to // handle ws connections. Please note that WsOnly field is used to understand to case while handling the incoming request if config.wsEndpoint() != "" && !strings.EqualFold(config.httpEndpoint(), config.wsEndpoint()) { + if err := validatePath(config.WsPathPrefix); err != nil { + logger.Fatal().Err(err).Msg("Websocket config err:") + } + httpCfg := rpc.HttpConfig{ HttpEndpoint: config.wsEndpoint(), + HttpPathPrefix: config.HttpPathPrefix, HttpCors: []string{}, HttpCompress: false, HttpTimeout: config.HttpTimeout, WsEndpoint: config.wsEndpoint(), + WsPathPrefix: config.WsPathPrefix, WsHandshakeTimeout: config.WsHandshakeTimeout, WsOnly: true, } @@ -77,3 +92,17 @@ func (n *RpcNode) Start() { } }() } + +// validatePath checks if 'path' is a valid configuration value for the RPC prefix option +func validatePath(path string) error { + if path == "*" || path == "" { + return nil + } + if path[0] != '/' { + return fmt.Errorf("RPC path prefix %q does not contain leading `/`", path) + } + if strings.ContainsAny(path, "?#") { + return fmt.Errorf("RPC path prefix %q contains URL meta-characters", path) + } + return nil +}