diff --git a/README.md b/README.md index 1b20481..fcd90ce 100644 --- a/README.md +++ b/README.md @@ -42,35 +42,65 @@ curl http://localhost:8080/chains/main/blocks/head/header Here a default `tzproxy.yaml` file: ```yaml +allow_routes: + enabled: true + values: + - GET/chains/.*/blocks + - GET/chains/.*/chain_id + - GET/chains.*/checkpoint + - GET/chains/.*/invalid_blocks + - GET/chains.*/invalid_blocks.* + - GET/chains/.*/is_bootstrapped + - GET/chains.*/mempool/filter + - GET/chains/.*/mempool/monitor_operations + - GET/chains/.*/mempool/pending_operations + - GET/config/network/user_activated_protocol_overrides + - GET/config/network/user_activated_upgrades + - GET/config/network/dal + - GET/describe.* + - GET/errors + - GET/monitor.* + - GET/network/greylist/ips + - GET/network/greylist/peers + - GET/network/self + - GET/network/stat + - GET/network/version + - GET/network/versions + - GET/protocols + - GET/protocols.* + - GET/protocols.*/environment + - GET/version + - POST/chains/.*/blocks/.*/helpers + - POST/chains/.*/blocks/.*/script + - POST/chains/.*/blocks/.*/context/contracts.*/big_map_get + - POST/injection/operation cache: disabled_routes: - - /monitor/.* - - /chains/.*/mempool - - /chains/.*/blocks.*head + - GET/monitor/.* + - GET/chains/.*/mempool + - GET/chains/.*/blocks.*head enabled: true size_mb: 100 ttl: 5 cors: enabled: true -deny_list: +deny_ips: enabled: false values: [] deny_routes: enabled: true values: - - /injection/block - - /injection/protocol - - /network.* - - /workers.* - - /worker.* - - /stats.* - - /config - - /chains/.*/blocks/.*/helpers/baking_rights - - /chains/.*/blocks/.*/helpers/endorsing_rights - - /helpers/baking_rights - - /helpers/endorsing_rights - - /chains/.*/blocks/.*/context/contracts(/?)$ - - /chains/.*/blocks/.*/context/raw/bytes + - GET/workers.* + - GET/worker.* + - GET/stats.* + - GET/chains/.*/blocks/.*/helpers/baking_rights + - GET/chains/.*/blocks/.*/helpers/endorsing_rights + - GET/helpers/baking_rights + - GET/helpers/endorsing_rights + - GET/chains/.*/blocks/.*/context/contracts(/?)$ + - GET/chains/.*/blocks/.*/context/raw/bytes + - POST/injection/block + - POST/injection/protocol dev_mode: false gc: optimize_memory_store: true @@ -96,6 +126,7 @@ redis: host: "" tezos_host: - 127.0.0.1:8732 +tezos_host_retry: "" ``` ### Environment Variables @@ -118,10 +149,12 @@ You can also configure or overwrite TzProxy with environment variables, using th - `TZPROXY_RATE_LIMIT_ENABLED` is a flag to enable rate limiting. - `TZPROXY_RATE_LIMIT_MINUTES` is the minutes of the period of rate limiting. - `TZPROXY_RATE_LIMIT_MAX` is the max of requests permitted in a period. -- `TZPROXY_DENY_LIST_ENABLED` is a flag to block IP addresses. -- `TZPROXY_DENY_LIST_VALUES` is the IP Address that will be blocked on the proxy. +- `TZPROXY_DENY_IPS_ENABLED` is a flag to block IP addresses. +- `TZPROXY_DENY_IPS_VALUES` is the IP Address that will be blocked on the proxy. - `TZPROXY_DENY_ROUTES_ENABLED` is a flag to block the Tezos node's routes. - `TZPROXY_DENY_ROUTES_VALUES` is the Tezos nodes routes that will be blocked on the proxy.conf. +- `TZPROXY_ALLOW_ROUTES_ENABLED` is a flag to allow the Tezos node's routes. +- `TZPROXY_ALLOW_ROUTES_VALUES` is the Tezos nodes routes that will be allowed on the proxy.conf. - `TZPROXY_METRICS_ENABLED` is the flag to enable metrics. - `TZPROXY_METRICS_PPROF` is the flag to enable pprof. - `TZPROXY_METRICS_HOST` is the host of the prometheus metrics and pprof (if enabled). diff --git a/config/config.go b/config/config.go index 8efde9e..40015d3 100644 --- a/config/config.go +++ b/config/config.go @@ -4,7 +4,6 @@ import ( "net/http" "net/url" "os" - "regexp" "strings" "time" @@ -68,11 +67,16 @@ func NewConfig() *Config { }, } + // Parse routes by http method + allowRegexRoutes := parseRegexRoutes(configFile.AllowRoutes.Values) + denyRegexRoutes := parseRegexRoutes(configFile.DenyRoutes.Values) + cacheDisableRegexRoutes := parseRegexRoutes(configFile.Cache.DisabledRoutes) + config := &Config{ ConfigFile: configFile, - DenyListTable: func() map[string]bool { + DenyIPsTable: func() map[string]bool { table := make(map[string]bool) - for _, ip := range configFile.DenyList.Values { + for _, ip := range configFile.DenyIPs.Values { table[ip] = true } return table @@ -81,28 +85,14 @@ func NewConfig() *Config { Period: time.Duration(configFile.RateLimit.Minutes) * time.Minute, Limit: int64(configFile.RateLimit.Max), }, - Store: store, - CacheTTL: time.Duration(configFile.Cache.TTL) * (time.Second), - ProxyConfig: &proxyConfig, - Redis: redisClient, - } - - for _, route := range config.ConfigFile.DenyRoutes.Values { - regex, err := regexp.Compile(route) - if err != nil { - log.Fatal().Err(err).Msg("unable to compile regex") - } - config.BlockRoutesRegex = append(config.BlockRoutesRegex, regex) + Store: store, + CacheTTL: time.Duration(configFile.Cache.TTL) * (time.Second), + ProxyConfig: &proxyConfig, + Redis: redisClient, + AllowRoutesRegex: allowRegexRoutes, + DenyRoutesRegex: denyRegexRoutes, + CacheDisabledRoutesRegex: cacheDisableRegexRoutes, } - - for _, route := range config.ConfigFile.Cache.DisabledRoutes { - regex, err := regexp.Compile(route) - if err != nil { - log.Fatal().Err(err).Msg("unable to compile regex") - } - config.CacheDisabledRoutesRegex = append(config.CacheDisabledRoutesRegex, regex) - } - config.Logger = logger config.RequestLoggerConfig = &middleware.RequestLoggerConfig{ diff --git a/config/default.go b/config/default.go index ba8274f..48cbe7f 100644 --- a/config/default.go +++ b/config/default.go @@ -25,25 +25,53 @@ var defaultConfig = &ConfigFile{ Enabled: true, TTL: 5, DisabledRoutes: []string{ - "/monitor/.*", - "/chains/.*/mempool", - "/chains/.*/blocks.*head", + "GET/monitor/.*", + "GET/chains/.*/mempool", + "GET/chains/.*/blocks.*head", }, SizeMB: 100, }, - DenyList: DenyList{ + DenyIPs: DenyIPs{ Enabled: false, Values: []string{}, }, + AllowRoutes: AllowRoutes{ + Enabled: true, + Values: []string{ + "GET/chains/.*/blocks", + "GET/chains/.*/chain_id", "GET/chains.*/checkpoint", + "GET/chains/.*/invalid_blocks", "GET/chains.*/invalid_blocks.*", + "GET/chains/.*/is_bootstrapped", "GET/chains.*/mempool/filter", + "GET/chains/.*/mempool/monitor_operations", + "GET/chains/.*/mempool/pending_operations", + "GET/config/network/user_activated_protocol_overrides", + "GET/config/network/user_activated_upgrades", + "GET/config/network/dal", "GET/describe.*", "GET/errors", + "GET/monitor.*", "GET/network/greylist/ips", + "GET/network/greylist/peers", "GET/network/self", + "GET/network/stat", "GET/network/version", "GET/network/versions", + "GET/protocols", "GET/protocols.*", "GET/protocols.*/environment", + "GET/version", + "POST/chains/.*/blocks/.*/helpers", + "POST/chains/.*/blocks/.*/script", + "POST/chains/.*/blocks/.*/context/contracts.*/big_map_get", + "POST/injection/operation", + }, + }, DenyRoutes: DenyRoutes{ Enabled: true, Values: []string{ - "/injection/block", "/injection/protocol", "/network.*", "/workers.*", - "/worker.*", "/stats.*", "/config", "/chains/.*/blocks/.*/helpers/baking_rights", - "/chains/.*/blocks/.*/helpers/endorsing_rights", - "/helpers/baking_rights", "/helpers/endorsing_rights", - "/chains/.*/blocks/.*/context/contracts(/?)$", - "/chains/.*/blocks/.*/context/raw/bytes", + "GET/workers.*", + "GET/worker.*", + "GET/stats.*", + "GET/chains/.*/blocks/.*/helpers/baking_rights", + "GET/chains/.*/blocks/.*/helpers/endorsing_rights", + "GET/helpers/baking_rights", + "GET/helpers/endorsing_rights", + "GET/chains/.*/blocks/.*/context/contracts(/?)$", + "GET/chains/.*/blocks/.*/context/raw/bytes", + "POST/injection/block", + "POST/injection/protocol", }, }, Metrics: Metrics{ diff --git a/config/model.go b/config/model.go index f8c2cef..0cf3361 100644 --- a/config/model.go +++ b/config/model.go @@ -15,10 +15,11 @@ type Config struct { Level uint HashBlock string ConfigFile *ConfigFile - DenyListTable map[string]bool Rate *limiter.Rate - CacheDisabledRoutesRegex []*regexp.Regexp - BlockRoutesRegex []*regexp.Regexp + DenyIPsTable map[string]bool + CacheDisabledRoutesRegex map[string][]*regexp.Regexp + DenyRoutesRegex map[string][]*regexp.Regexp + AllowRoutesRegex map[string][]*regexp.Regexp Store echocache.Cache CacheTTL time.Duration RequestLoggerConfig *middleware.RequestLoggerConfig @@ -45,7 +46,12 @@ type Cache struct { SizeMB int `mapstructure:"size_mb"` } -type DenyList struct { +type DenyIPs struct { + Enabled bool `mapstructure:"enabled"` + Values []string `mapstructure:"values"` +} + +type AllowRoutes struct { Enabled bool `mapstructure:"enabled"` Values []string `mapstructure:"values"` } @@ -90,8 +96,9 @@ type ConfigFile struct { Logger Logger `mapstructure:"logger"` RateLimit RateLimit `mapstructure:"rate_limit"` Cache Cache `mapstructure:"cache"` - DenyList DenyList `mapstructure:"deny_list"` + DenyIPs DenyIPs `mapstructure:"deny_ips"` DenyRoutes DenyRoutes `mapstructure:"deny_routes"` + AllowRoutes AllowRoutes `mapstructure:"allow_routes"` Metrics Metrics `mapstructure:"metrics"` GC GC `mapstructure:"gc"` CORS CORS `mapstructure:"cors"` diff --git a/config/regex.go b/config/regex.go new file mode 100644 index 0000000..76eed08 --- /dev/null +++ b/config/regex.go @@ -0,0 +1,56 @@ +package config + +import ( + "net/http" + "regexp" + "strings" + + "github.com/rs/zerolog/log" +) + +func parseRegexRoutes(values []string) map[string][]*regexp.Regexp { + allMethods := []string{ + http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, + http.MethodPatch, http.MethodHead, http.MethodOptions, + } + + regexRoutes := make(map[string][]*regexp.Regexp) + for _, route := range values { + if !containsPrefix(route, allMethods) { + for _, method := range allMethods { + regex, err := regexp.Compile(route) + if err != nil { + log.Fatal().Err(err).Msg("unable to compile regex") + } + + regexRoutes[method] = append(regexRoutes[method], regex) + } + continue + } + + for _, method := range allMethods { + if strings.HasPrefix(strings.ToUpper(route), method) { + url := strings.TrimPrefix(route, method) + regex, err := regexp.Compile(url) + if err != nil { + log.Fatal().Err(err).Msg("unable to compile regex") + } + + regexRoutes[method] = append(regexRoutes[method], regex) + break + } + } + } + + return regexRoutes +} + +func containsPrefix(s string, prefixes []string) bool { + for _, prefix := range prefixes { + if strings.HasPrefix(s, prefix) { + return true + } + } + + return false +} diff --git a/config/viper.go b/config/viper.go index 99e404d..12ee35d 100644 --- a/config/viper.go +++ b/config/viper.go @@ -46,10 +46,12 @@ func initViper() *ConfigFile { viper.SetDefault("rate_limit.enabled", defaultConfig.RateLimit.Enabled) viper.SetDefault("rate_limit.minutes", defaultConfig.RateLimit.Minutes) viper.SetDefault("rate_limit.max", defaultConfig.RateLimit.Max) - viper.SetDefault("deny_list.enabled", defaultConfig.DenyList.Enabled) - viper.SetDefault("deny_list.values", defaultConfig.DenyList.Values) + viper.SetDefault("deny_ips.enabled", defaultConfig.DenyIPs.Enabled) + viper.SetDefault("deny_ips.values", defaultConfig.DenyIPs.Values) viper.SetDefault("deny_routes.enabled", defaultConfig.DenyRoutes.Enabled) viper.SetDefault("deny_routes.values", defaultConfig.DenyRoutes.Values) + viper.SetDefault("allow_routes.enabled", defaultConfig.AllowRoutes.Enabled) + viper.SetDefault("allow_routes.values", defaultConfig.AllowRoutes.Values) viper.SetDefault("metrics.enabled", defaultConfig.Metrics.Enabled) viper.SetDefault("metrics.pprof", defaultConfig.Metrics.Pprof) viper.SetDefault("metrics.host", defaultConfig.Metrics.Host) diff --git a/flextesa.sh b/flextesa.sh index d2fd8e5..8b3a40b 100755 --- a/flextesa.sh +++ b/flextesa.sh @@ -1,5 +1,5 @@ image=oxheadalpha/flextesa:latest -script=mumbaibox +script=nairobibox docker run --rm --name my-sandbox --detach -p 8732:20000 \ -e block_time=3 \ "$image" "$script" start diff --git a/main.go b/main.go index 1300b31..5a87dae 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,8 @@ func main() { e.Use(middleware.RequestLoggerWithConfig(*config.RequestLoggerConfig)) e.Use(middlewares.CORS(config)) e.Use(middlewares.RateLimit(config)) + e.Use(middlewares.DenyIPs(config)) + e.Use(middlewares.AllowRoutes(config)) e.Use(middlewares.DenyRoutes(config)) e.Use(middlewares.Cache(config)) e.Use(middlewares.Gzip(config)) diff --git a/middlewares/allowroutes.go b/middlewares/allowroutes.go new file mode 100644 index 0000000..4e87042 --- /dev/null +++ b/middlewares/allowroutes.go @@ -0,0 +1,49 @@ +package middlewares + +import ( + "fmt" + "net/http" + + "github.com/labstack/echo/v4" + "github.com/marigold-dev/tzproxy/config" +) + +func AllowRoutes(config *config.Config) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) (err error) { + if !config.ConfigFile.AllowRoutes.Enabled { + return next(c) + } + + r := c.Request() + + path := r.URL.Path + + if r.Method == http.MethodOptions { + return next(c) + } + + regexRoutesByMethod, has := config.AllowRoutesRegex[r.Method] + if !has { + msg := fmt.Sprintf("You don't have access %s route", path) + return c.JSON(http.StatusForbidden, echo.Map{ + "success": false, + "message": msg, + }) + } + + for _, regex := range regexRoutesByMethod { + if regex.MatchString(path) { + return next(c) + } + } + + msg := fmt.Sprintf("You don't have access %s route", path) + return c.JSON(http.StatusForbidden, echo.Map{ + "success": false, + "message": msg, + }) + + } + } +} diff --git a/middlewares/cache.go b/middlewares/cache.go index a0eee15..41a5540 100644 --- a/middlewares/cache.go +++ b/middlewares/cache.go @@ -21,7 +21,12 @@ func Cache(config *config.Config) echo.MiddlewareFunc { return false } - for _, regex := range config.CacheDisabledRoutesRegex { + regexRoutesByMethod, has := config.CacheDisabledRoutesRegex[r.Method] + if !has { + return true + } + + for _, regex := range regexRoutesByMethod { if regex.MatchString(r.URL.Path) { return false } diff --git a/middlewares/denylist.go b/middlewares/denyips.go similarity index 73% rename from middlewares/denylist.go rename to middlewares/denyips.go index bba31c7..1d1d02d 100644 --- a/middlewares/denylist.go +++ b/middlewares/denyips.go @@ -7,14 +7,14 @@ import ( "github.com/marigold-dev/tzproxy/config" ) -func Denylist(config *config.Config) echo.MiddlewareFunc { +func DenyIPs(config *config.Config) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) (err error) { - if !config.ConfigFile.DenyList.Enabled { + if !config.ConfigFile.DenyIPs.Enabled { return next(c) } - value, has := config.DenyListTable[c.RealIP()] + value, has := config.DenyIPsTable[c.RealIP()] if value && has { return c.JSON(http.StatusForbidden, echo.Map{ "success": false, diff --git a/middlewares/denyroutes.go b/middlewares/denyroutes.go index 4f56ecb..937db35 100644 --- a/middlewares/denyroutes.go +++ b/middlewares/denyroutes.go @@ -15,9 +15,16 @@ func DenyRoutes(config *config.Config) echo.MiddlewareFunc { return next(c) } - path := c.Request().URL.Path + r := c.Request() - for _, regex := range config.BlockRoutesRegex { + path := r.URL.Path + + regexRoutesByMethod, has := config.DenyRoutesRegex[r.Method] + if !has { + return next(c) + } + + for _, regex := range regexRoutesByMethod { if regex.MatchString(path) { msg := fmt.Sprintf("You don't have access %s route", path) return c.JSON(http.StatusForbidden, echo.Map{ diff --git a/tzproxy.yaml b/tzproxy.yaml index f3abba3..f10c5e7 100644 --- a/tzproxy.yaml +++ b/tzproxy.yaml @@ -1,32 +1,62 @@ +allow_routes: + enabled: true + values: + - GET/chains/.*/blocks + - GET/chains/.*/chain_id + - GET/chains.*/checkpoint + - GET/chains/.*/invalid_blocks + - GET/chains.*/invalid_blocks.* + - GET/chains/.*/is_bootstrapped + - GET/chains.*/mempool/filter + - GET/chains/.*/mempool/monitor_operations + - GET/chains/.*/mempool/pending_operations + - GET/config/network/user_activated_protocol_overrides + - GET/config/network/user_activated_upgrades + - GET/config/network/dal + - GET/describe.* + - GET/errors + - GET/monitor.* + - GET/network/greylist/ips + - GET/network/greylist/peers + - GET/network/self + - GET/network/stat + - GET/network/version + - GET/network/versions + - GET/protocols + - GET/protocols.* + - GET/protocols.*/environment + - GET/version + - POST/chains/.*/blocks/.*/helpers + - POST/chains/.*/blocks/.*/script + - POST/chains/.*/blocks/.*/context/contracts.*/big_map_get + - POST/injection/operation cache: disabled_routes: - - /monitor/.* - - /chains/.*/mempool - - /chains/.*/blocks.*head - enabled: false + - GET/monitor/.* + - GET/chains/.*/mempool + - GET/chains/.*/blocks.*head + enabled: true size_mb: 100 ttl: 5 cors: enabled: true -deny_list: +deny_ips: enabled: false values: [] deny_routes: enabled: true values: - - /injection/block - - /injection/protocol - - /network.* - - /workers.* - - /worker.* - - /stats.* - - /config - - /chains/.*/blocks/.*/helpers/baking_rights - - /chains/.*/blocks/.*/helpers/endorsing_rights - - /helpers/baking_rights - - /helpers/endorsing_rights - - /chains/.*/blocks/.*/context/contracts(/?)$ - - /chains/.*/blocks/.*/context/raw/bytes + - GET/workers.* + - GET/worker.* + - GET/stats.* + - GET/chains/.*/blocks/.*/helpers/baking_rights + - GET/chains/.*/blocks/.*/helpers/endorsing_rights + - GET/helpers/baking_rights + - GET/helpers/endorsing_rights + - GET/chains/.*/blocks/.*/context/contracts(/?)$ + - GET/chains/.*/blocks/.*/context/raw/bytes + - POST/injection/block + - POST/injection/protocol dev_mode: false gc: optimize_memory_store: true