diff --git a/.env b/.env index 082296d..33b42ed 100644 --- a/.env +++ b/.env @@ -132,7 +132,14 @@ CACHE_INDEFINITELY=false # CACHE_PREFIX must not contain colon symbol CACHE_PREFIX=local-chain # WHITELISTED_HEADERS contains comma-separated list of headers which has to be cached along with EVM JSON-RPC response -WHITELISTED_HEADERS=Server,Vary,Access-Control-Expose-Headers,Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Credentials,Access-Control-Max-Age +WHITELISTED_HEADERS=Vary,Access-Control-Expose-Headers,Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Credentials,Access-Control-Max-Age +# DEFAULT_ACCESS_CONTROL_ALLOW_ORIGIN_VALUE contains default value for Access-Control-Allow-Origin header. +# NOTE: it will be used only in Cache Hit scenario. +DEFAULT_ACCESS_CONTROL_ALLOW_ORIGIN_VALUE="*" +# Map contains mapping between hostname (for ex. evm.kava.io) and corresponding value for Access-Control-Allow-Origin header. +# If hostname for specific request is missing we fallback to DEFAULT_ACCESS_CONTROL_ALLOW_ORIGIN_VALUE. +# NOTE: it will be used only in Cache Hit scenario. +HOSTNAME_TO_ACCESS_CONTROL_ALLOW_ORIGIN_VALUE_MAP= ##### Database Config POSTGRES_PASSWORD=password diff --git a/config/config.go b/config/config.go index 57ea0c9..0552734 100644 --- a/config/config.go +++ b/config/config.go @@ -13,44 +13,47 @@ import ( ) type Config struct { - ProxyServicePort string - LogLevel string - ProxyBackendHostURLMapRaw string - ProxyBackendHostURLMapParsed map[string]url.URL - EnableHeightBasedRouting bool - ProxyPruningBackendHostURLMapRaw string - ProxyPruningBackendHostURLMap map[string]url.URL - EvmQueryServiceURL string - DatabaseName string - DatabaseEndpointURL string - DatabaseUserName string - DatabasePassword string - DatabaseReadTimeoutSeconds int64 - DatabaseWriteTimeoutSeconds int64 - DatabaseSSLEnabled bool - DatabaseQueryLoggingEnabled bool - DatabaseMaxIdleConnections int64 - DatabaseConnectionMaxIdleSeconds int64 - DatabaseMaxOpenConnections int64 - RunDatabaseMigrations bool - HTTPReadTimeoutSeconds int64 - HTTPWriteTimeoutSeconds int64 - MetricCompactionRoutineInterval time.Duration - MetricCollectionEnabled bool - MetricPartitioningRoutineInterval time.Duration - MetricPartitioningRoutineDelayFirstRun time.Duration - MetricPartitioningPrefillPeriodDays int - MetricPruningEnabled bool - MetricPruningRoutineInterval time.Duration - MetricPruningRoutineDelayFirstRun time.Duration - MetricPruningMaxRequestMetricsHistoryDays int - CacheEnabled bool - CacheIndefinitely bool - RedisEndpointURL string - RedisPassword string - CacheTTL time.Duration - CachePrefix string - WhitelistedHeaders []string + ProxyServicePort string + LogLevel string + ProxyBackendHostURLMapRaw string + ProxyBackendHostURLMapParsed map[string]url.URL + EnableHeightBasedRouting bool + ProxyPruningBackendHostURLMapRaw string + ProxyPruningBackendHostURLMap map[string]url.URL + EvmQueryServiceURL string + DatabaseName string + DatabaseEndpointURL string + DatabaseUserName string + DatabasePassword string + DatabaseReadTimeoutSeconds int64 + DatabaseWriteTimeoutSeconds int64 + DatabaseSSLEnabled bool + DatabaseQueryLoggingEnabled bool + DatabaseMaxIdleConnections int64 + DatabaseConnectionMaxIdleSeconds int64 + DatabaseMaxOpenConnections int64 + RunDatabaseMigrations bool + HTTPReadTimeoutSeconds int64 + HTTPWriteTimeoutSeconds int64 + MetricCompactionRoutineInterval time.Duration + MetricCollectionEnabled bool + MetricPartitioningRoutineInterval time.Duration + MetricPartitioningRoutineDelayFirstRun time.Duration + MetricPartitioningPrefillPeriodDays int + MetricPruningEnabled bool + MetricPruningRoutineInterval time.Duration + MetricPruningRoutineDelayFirstRun time.Duration + MetricPruningMaxRequestMetricsHistoryDays int + CacheEnabled bool + CacheIndefinitely bool + RedisEndpointURL string + RedisPassword string + CacheTTL time.Duration + CachePrefix string + WhitelistedHeaders []string + DefaultAccessControlAllowOriginValue string + HostnameToAccessControlAllowOriginValueMapRaw string + HostnameToAccessControlAllowOriginValueMap map[string]string } const ( @@ -88,32 +91,37 @@ const ( DEFAULT_METRIC_PRUNING_ENABLED = true METRIC_PRUNING_ROUTINE_INTERVAL_SECONDS_ENVIRONMENT_KEY = "METRIC_PRUNING_ROUTINE_INTERVAL_SECONDS" // 60 seconds * 60 minutes * 24 hours = 1 day - DEFAULT_METRIC_PRUNING_ROUTINE_INTERVAL_SECONDS = 86400 - METRIC_PRUNING_ROUTINE_DELAY_FIRST_RUN_SECONDS_ENVIRONMENT_KEY = "METRIC_PRUNING_ROUTINE_DELAY_FIRST_RUN_SECONDS" - DEFAULT_METRIC_PRUNING_ROUTINE_DELAY_FIRST_RUN_SECONDS = 10 - METRIC_PRUNING_MAX_REQUEST_METRICS_HISTORY_DAYS_ENVIRONMENT_KEY = "METRIC_PRUNING_MAX_REQUEST_METRICS_HISTORY_DAYS" - DEFAULT_METRIC_PRUNING_MAX_REQUEST_METRICS_HISTORY_DAYS = 45 - EVM_QUERY_SERVICE_ENVIRONMENT_KEY = "EVM_QUERY_SERVICE_URL" - DATABASE_MAX_IDLE_CONNECTIONS_ENVIRONMENT_KEY = "DATABASE_MAX_IDLE_CONNECTIONS" - DEFAULT_DATABASE_MAX_IDLE_CONNECTIONS = 20 - DATABASE_CONNECTION_MAX_IDLE_SECONDS_ENVIRONMENT_KEY = "DATABASE_CONNECTION_MAX_IDLE_SECONDS" - DEFAULT_DATABASE_CONNECTION_MAX_IDLE_SECONDS = 5 - DATABASE_MAX_OPEN_CONNECTIONS_ENVIRONMENT_KEY = "DATABASE_MAX_OPEN_CONNECTIONS" - DEFAULT_DATABASE_MAX_OPEN_CONNECTIONS = 100 - DATABASE_READ_TIMEOUT_SECONDS_ENVIRONMENT_KEY = "DATABASE_READ_TIMEOUT_SECONDS" - DEFAULT_DATABASE_READ_TIMEOUT_SECONDS = 60 - DATABASE_WRITE_TIMEOUT_SECONDS_ENVIRONMENT_KEY = "DATABASE_WRITE_TIMEOUT_SECONDS" - DEFAULT_DATABASE_WRITE_TIMEOUT_SECONDS = 10 - CACHE_ENABLED_ENVIRONMENT_KEY = "CACHE_ENABLED" - REDIS_ENDPOINT_URL_ENVIRONMENT_KEY = "REDIS_ENDPOINT_URL" - REDIS_PASSWORD_ENVIRONMENT_KEY = "REDIS_PASSWORD" - CACHE_INDEFINITELY_KEY = "CACHE_INDEFINITELY" - CACHE_TTL_ENVIRONMENT_KEY = "CACHE_TTL_SECONDS" - CACHE_PREFIX_ENVIRONMENT_KEY = "CACHE_PREFIX" - WHITELISTED_HEADERS_ENVIRONMENT_KEY = "WHITELISTED_HEADERS" + DEFAULT_METRIC_PRUNING_ROUTINE_INTERVAL_SECONDS = 86400 + METRIC_PRUNING_ROUTINE_DELAY_FIRST_RUN_SECONDS_ENVIRONMENT_KEY = "METRIC_PRUNING_ROUTINE_DELAY_FIRST_RUN_SECONDS" + DEFAULT_METRIC_PRUNING_ROUTINE_DELAY_FIRST_RUN_SECONDS = 10 + METRIC_PRUNING_MAX_REQUEST_METRICS_HISTORY_DAYS_ENVIRONMENT_KEY = "METRIC_PRUNING_MAX_REQUEST_METRICS_HISTORY_DAYS" + DEFAULT_METRIC_PRUNING_MAX_REQUEST_METRICS_HISTORY_DAYS = 45 + EVM_QUERY_SERVICE_ENVIRONMENT_KEY = "EVM_QUERY_SERVICE_URL" + DATABASE_MAX_IDLE_CONNECTIONS_ENVIRONMENT_KEY = "DATABASE_MAX_IDLE_CONNECTIONS" + DEFAULT_DATABASE_MAX_IDLE_CONNECTIONS = 20 + DATABASE_CONNECTION_MAX_IDLE_SECONDS_ENVIRONMENT_KEY = "DATABASE_CONNECTION_MAX_IDLE_SECONDS" + DEFAULT_DATABASE_CONNECTION_MAX_IDLE_SECONDS = 5 + DATABASE_MAX_OPEN_CONNECTIONS_ENVIRONMENT_KEY = "DATABASE_MAX_OPEN_CONNECTIONS" + DEFAULT_DATABASE_MAX_OPEN_CONNECTIONS = 100 + DATABASE_READ_TIMEOUT_SECONDS_ENVIRONMENT_KEY = "DATABASE_READ_TIMEOUT_SECONDS" + DEFAULT_DATABASE_READ_TIMEOUT_SECONDS = 60 + DATABASE_WRITE_TIMEOUT_SECONDS_ENVIRONMENT_KEY = "DATABASE_WRITE_TIMEOUT_SECONDS" + DEFAULT_DATABASE_WRITE_TIMEOUT_SECONDS = 10 + CACHE_ENABLED_ENVIRONMENT_KEY = "CACHE_ENABLED" + REDIS_ENDPOINT_URL_ENVIRONMENT_KEY = "REDIS_ENDPOINT_URL" + REDIS_PASSWORD_ENVIRONMENT_KEY = "REDIS_PASSWORD" + CACHE_INDEFINITELY_KEY = "CACHE_INDEFINITELY" + CACHE_TTL_ENVIRONMENT_KEY = "CACHE_TTL_SECONDS" + CACHE_PREFIX_ENVIRONMENT_KEY = "CACHE_PREFIX" + WHITELISTED_HEADERS_ENVIRONMENT_KEY = "WHITELISTED_HEADERS" + DEFAULT_ACCESS_CONTROL_ALLOW_ORIGIN_VALUE_ENVIRONMENT_KEY = "DEFAULT_ACCESS_CONTROL_ALLOW_ORIGIN_VALUE" + HOSTNAME_TO_ACCESS_CONTROL_ALLOW_ORIGIN_VALUE_MAP_ENVIRONMENT_KEY = "HOSTNAME_TO_ACCESS_CONTROL_ALLOW_ORIGIN_VALUE_MAP" ) -var ErrEmptyHostMap = errors.New("backend host url map is empty") +var ( + ErrEmptyHostMap = errors.New("backend host url map is empty") + ErrEmptyHostnameToHeaderValueMap = errors.New("hostname to header value map is empty") +) // EnvOrDefault fetches an environment variable value, or if not set returns the fallback value func EnvOrDefault(key string, fallback string) string { @@ -205,6 +213,37 @@ func ParseRawProxyBackendHostURLMap(raw string) (map[string]url.URL, error) { return hostURLMap, combinedErr } +// ParseRawHostnameToHeaderValueMap attempts to parse mappings of hostname to corresponding header value. +// For example hostname to access-control-allow-origin header value. +func ParseRawHostnameToHeaderValueMap(raw string) (map[string]string, error) { + hostnameToHeaderValueMap := map[string]string{} + var combinedErr error + + entries := strings.Split(raw, PROXY_BACKEND_HOST_URL_MAP_ENTRY_DELIMITER) + + if raw == "" || len(entries) < 1 { + extraErr := fmt.Errorf("found zero mappings delimited by %s in %s", PROXY_BACKEND_HOST_URL_MAP_ENTRY_DELIMITER, raw) + return hostnameToHeaderValueMap, errors.Join(ErrEmptyHostnameToHeaderValueMap, extraErr) + } + + for _, entry := range entries { + entryComponents := strings.Split(entry, PROXY_BACKEND_HOST_URL_MAP_SUB_COMPONENT_DELIMITER) + + if len(entryComponents) != 2 { + combinedErr = errors.Join(combinedErr, fmt.Errorf("expected map value of hostname to header value delimited by %s, got %s", PROXY_BACKEND_HOST_URL_MAP_SUB_COMPONENT_DELIMITER, entry)) + + continue + } + + hostname := entryComponents[0] + headerValue := entryComponents[1] + + hostnameToHeaderValueMap[hostname] = headerValue + } + + return hostnameToHeaderValueMap, combinedErr +} + // ReadConfig attempts to parse service config from environment values // the returned config may be invalid and should be validated via the `Validate` // function of the Config package before use @@ -223,44 +262,61 @@ func ReadConfig() Config { parsedWhitelistedHeaders = []string{} } + rawHostnameToAccessControlAllowOriginValueMap := os.Getenv(HOSTNAME_TO_ACCESS_CONTROL_ALLOW_ORIGIN_VALUE_MAP_ENVIRONMENT_KEY) + // best effort to parse, callers are responsible for validating + // before using any values read + parsedHostnameToAccessControlAllowOriginValueMap, _ := ParseRawHostnameToHeaderValueMap(rawHostnameToAccessControlAllowOriginValueMap) + return Config{ - ProxyServicePort: os.Getenv(PROXY_SERVICE_PORT_ENVIRONMENT_KEY), - LogLevel: EnvOrDefault(LOG_LEVEL_ENVIRONMENT_KEY, DEFAULT_LOG_LEVEL), - ProxyBackendHostURLMapRaw: rawProxyBackendHostURLMap, - ProxyBackendHostURLMapParsed: parsedProxyBackendHostURLMap, - EnableHeightBasedRouting: EnvOrDefaultBool(PROXY_HEIGHT_BASED_ROUTING_ENABLED_KEY, false), - ProxyPruningBackendHostURLMapRaw: rawProxyPruningBackendHostURLMap, - ProxyPruningBackendHostURLMap: parsedProxyPruningBackendHostURLMap, - DatabaseName: os.Getenv(DATABASE_NAME_ENVIRONMENT_KEY), - DatabaseEndpointURL: os.Getenv(DATABASE_ENDPOINT_URL_ENVIRONMENT_KEY), - DatabaseUserName: os.Getenv(DATABASE_USERNAME_ENVIRONMENT_KEY), - DatabasePassword: os.Getenv(DATABASE_PASSWORD_ENVIRONMENT_KEY), - DatabaseSSLEnabled: EnvOrDefaultBool(DATABASE_SSL_ENABLED_ENVIRONMENT_KEY, false), - DatabaseReadTimeoutSeconds: EnvOrDefaultInt64(DATABASE_READ_TIMEOUT_SECONDS_ENVIRONMENT_KEY, DEFAULT_DATABASE_READ_TIMEOUT_SECONDS), - DatabaseWriteTimeoutSeconds: EnvOrDefaultInt64(DATABASE_WRITE_TIMEOUT_SECONDS_ENVIRONMENT_KEY, DEFAULT_DATABASE_WRITE_TIMEOUT_SECONDS), - DatabaseQueryLoggingEnabled: EnvOrDefaultBool(DATABASE_QUERY_LOGGING_ENABLED_ENVIRONMENT_KEY, true), - RunDatabaseMigrations: EnvOrDefaultBool(RUN_DATABASE_MIGRATIONS_ENVIRONMENT_KEY, false), - DatabaseMaxIdleConnections: EnvOrDefaultInt64(DATABASE_MAX_IDLE_CONNECTIONS_ENVIRONMENT_KEY, DEFAULT_DATABASE_MAX_IDLE_CONNECTIONS), - DatabaseConnectionMaxIdleSeconds: EnvOrDefaultInt64(DATABASE_CONNECTION_MAX_IDLE_SECONDS_ENVIRONMENT_KEY, DEFAULT_DATABASE_CONNECTION_MAX_IDLE_SECONDS), - DatabaseMaxOpenConnections: EnvOrDefaultInt64(DATABASE_MAX_OPEN_CONNECTIONS_ENVIRONMENT_KEY, DEFAULT_DATABASE_MAX_OPEN_CONNECTIONS), - HTTPReadTimeoutSeconds: EnvOrDefaultInt64(HTTP_READ_TIMEOUT_ENVIRONMENT_KEY, DEFAULT_HTTP_READ_TIMEOUT), - HTTPWriteTimeoutSeconds: EnvOrDefaultInt64(HTTP_WRITE_TIMEOUT_ENVIRONMENT_KEY, DEFAULT_HTTP_WRITE_TIMEOUT), - MetricCompactionRoutineInterval: time.Duration(time.Duration(EnvOrDefaultInt(METRIC_COMPACTION_ROUTINE_INTERVAL_ENVIRONMENT_KEY, DEFAULT_METRIC_COMPACTION_ROUTINE_INTERVAL_SECONDS)) * time.Second), - EvmQueryServiceURL: os.Getenv(EVM_QUERY_SERVICE_ENVIRONMENT_KEY), - MetricCollectionEnabled: EnvOrDefaultBool(METRIC_COLLECTION_ENABLED_ENVIRONMENT_KEY, DEFAULT_METRIC_COLLECTION_ENABLED), - MetricPartitioningRoutineInterval: time.Duration(time.Duration(EnvOrDefaultInt(METRIC_PARTITIONING_ROUTINE_INTERVAL_SECONDS_ENVIRONMENT_KEY, DEFAULT_METRIC_PARTITIONING_ROUTINE_INTERVAL_SECONDS)) * time.Second), - MetricPartitioningRoutineDelayFirstRun: time.Duration(time.Duration(EnvOrDefaultInt(METRIC_PARTITIONING_ROUTINE_DELAY_FIRST_RUN_SECONDS_ENVIRONMENT_KEY, DEFAULT_METRIC_PARTITIONING_ROUTINE_DELAY_FIRST_RUN_SECONDS)) * time.Second), - MetricPartitioningPrefillPeriodDays: EnvOrDefaultInt(METRIC_PARTITIONING_PREFILL_PERIOD_DAYS_ENVIRONMENT_KEY, DEFAULT_METRIC_PARTITIONING_PREFILL_PERIOD_DAYS), - MetricPruningEnabled: EnvOrDefaultBool(METRIC_PRUNING_ENABLED_ENVIRONMENT_KEY, DEFAULT_METRIC_PRUNING_ENABLED), - MetricPruningRoutineInterval: time.Duration(time.Duration(EnvOrDefaultInt(METRIC_PRUNING_ROUTINE_INTERVAL_SECONDS_ENVIRONMENT_KEY, DEFAULT_METRIC_PRUNING_ROUTINE_INTERVAL_SECONDS)) * time.Second), - MetricPruningRoutineDelayFirstRun: time.Duration(time.Duration(EnvOrDefaultInt(METRIC_PRUNING_ROUTINE_DELAY_FIRST_RUN_SECONDS_ENVIRONMENT_KEY, DEFAULT_METRIC_PRUNING_ROUTINE_DELAY_FIRST_RUN_SECONDS)) * time.Second), - MetricPruningMaxRequestMetricsHistoryDays: EnvOrDefaultInt(METRIC_PRUNING_MAX_REQUEST_METRICS_HISTORY_DAYS_ENVIRONMENT_KEY, DEFAULT_METRIC_PRUNING_MAX_REQUEST_METRICS_HISTORY_DAYS), - CacheEnabled: EnvOrDefaultBool(CACHE_ENABLED_ENVIRONMENT_KEY, false), - RedisEndpointURL: os.Getenv(REDIS_ENDPOINT_URL_ENVIRONMENT_KEY), - RedisPassword: os.Getenv(REDIS_PASSWORD_ENVIRONMENT_KEY), - CacheTTL: time.Duration(EnvOrDefaultInt(CACHE_TTL_ENVIRONMENT_KEY, 0)) * time.Second, - CacheIndefinitely: EnvOrDefaultBool(CACHE_INDEFINITELY_KEY, false), - CachePrefix: os.Getenv(CACHE_PREFIX_ENVIRONMENT_KEY), - WhitelistedHeaders: parsedWhitelistedHeaders, + ProxyServicePort: os.Getenv(PROXY_SERVICE_PORT_ENVIRONMENT_KEY), + LogLevel: EnvOrDefault(LOG_LEVEL_ENVIRONMENT_KEY, DEFAULT_LOG_LEVEL), + ProxyBackendHostURLMapRaw: rawProxyBackendHostURLMap, + ProxyBackendHostURLMapParsed: parsedProxyBackendHostURLMap, + EnableHeightBasedRouting: EnvOrDefaultBool(PROXY_HEIGHT_BASED_ROUTING_ENABLED_KEY, false), + ProxyPruningBackendHostURLMapRaw: rawProxyPruningBackendHostURLMap, + ProxyPruningBackendHostURLMap: parsedProxyPruningBackendHostURLMap, + DatabaseName: os.Getenv(DATABASE_NAME_ENVIRONMENT_KEY), + DatabaseEndpointURL: os.Getenv(DATABASE_ENDPOINT_URL_ENVIRONMENT_KEY), + DatabaseUserName: os.Getenv(DATABASE_USERNAME_ENVIRONMENT_KEY), + DatabasePassword: os.Getenv(DATABASE_PASSWORD_ENVIRONMENT_KEY), + DatabaseSSLEnabled: EnvOrDefaultBool(DATABASE_SSL_ENABLED_ENVIRONMENT_KEY, false), + DatabaseReadTimeoutSeconds: EnvOrDefaultInt64(DATABASE_READ_TIMEOUT_SECONDS_ENVIRONMENT_KEY, DEFAULT_DATABASE_READ_TIMEOUT_SECONDS), + DatabaseWriteTimeoutSeconds: EnvOrDefaultInt64(DATABASE_WRITE_TIMEOUT_SECONDS_ENVIRONMENT_KEY, DEFAULT_DATABASE_WRITE_TIMEOUT_SECONDS), + DatabaseQueryLoggingEnabled: EnvOrDefaultBool(DATABASE_QUERY_LOGGING_ENABLED_ENVIRONMENT_KEY, true), + RunDatabaseMigrations: EnvOrDefaultBool(RUN_DATABASE_MIGRATIONS_ENVIRONMENT_KEY, false), + DatabaseMaxIdleConnections: EnvOrDefaultInt64(DATABASE_MAX_IDLE_CONNECTIONS_ENVIRONMENT_KEY, DEFAULT_DATABASE_MAX_IDLE_CONNECTIONS), + DatabaseConnectionMaxIdleSeconds: EnvOrDefaultInt64(DATABASE_CONNECTION_MAX_IDLE_SECONDS_ENVIRONMENT_KEY, DEFAULT_DATABASE_CONNECTION_MAX_IDLE_SECONDS), + DatabaseMaxOpenConnections: EnvOrDefaultInt64(DATABASE_MAX_OPEN_CONNECTIONS_ENVIRONMENT_KEY, DEFAULT_DATABASE_MAX_OPEN_CONNECTIONS), + HTTPReadTimeoutSeconds: EnvOrDefaultInt64(HTTP_READ_TIMEOUT_ENVIRONMENT_KEY, DEFAULT_HTTP_READ_TIMEOUT), + HTTPWriteTimeoutSeconds: EnvOrDefaultInt64(HTTP_WRITE_TIMEOUT_ENVIRONMENT_KEY, DEFAULT_HTTP_WRITE_TIMEOUT), + MetricCompactionRoutineInterval: time.Duration(time.Duration(EnvOrDefaultInt(METRIC_COMPACTION_ROUTINE_INTERVAL_ENVIRONMENT_KEY, DEFAULT_METRIC_COMPACTION_ROUTINE_INTERVAL_SECONDS)) * time.Second), + EvmQueryServiceURL: os.Getenv(EVM_QUERY_SERVICE_ENVIRONMENT_KEY), + MetricCollectionEnabled: EnvOrDefaultBool(METRIC_COLLECTION_ENABLED_ENVIRONMENT_KEY, DEFAULT_METRIC_COLLECTION_ENABLED), + MetricPartitioningRoutineInterval: time.Duration(time.Duration(EnvOrDefaultInt(METRIC_PARTITIONING_ROUTINE_INTERVAL_SECONDS_ENVIRONMENT_KEY, DEFAULT_METRIC_PARTITIONING_ROUTINE_INTERVAL_SECONDS)) * time.Second), + MetricPartitioningRoutineDelayFirstRun: time.Duration(time.Duration(EnvOrDefaultInt(METRIC_PARTITIONING_ROUTINE_DELAY_FIRST_RUN_SECONDS_ENVIRONMENT_KEY, DEFAULT_METRIC_PARTITIONING_ROUTINE_DELAY_FIRST_RUN_SECONDS)) * time.Second), + MetricPartitioningPrefillPeriodDays: EnvOrDefaultInt(METRIC_PARTITIONING_PREFILL_PERIOD_DAYS_ENVIRONMENT_KEY, DEFAULT_METRIC_PARTITIONING_PREFILL_PERIOD_DAYS), + MetricPruningEnabled: EnvOrDefaultBool(METRIC_PRUNING_ENABLED_ENVIRONMENT_KEY, DEFAULT_METRIC_PRUNING_ENABLED), + MetricPruningRoutineInterval: time.Duration(time.Duration(EnvOrDefaultInt(METRIC_PRUNING_ROUTINE_INTERVAL_SECONDS_ENVIRONMENT_KEY, DEFAULT_METRIC_PRUNING_ROUTINE_INTERVAL_SECONDS)) * time.Second), + MetricPruningRoutineDelayFirstRun: time.Duration(time.Duration(EnvOrDefaultInt(METRIC_PRUNING_ROUTINE_DELAY_FIRST_RUN_SECONDS_ENVIRONMENT_KEY, DEFAULT_METRIC_PRUNING_ROUTINE_DELAY_FIRST_RUN_SECONDS)) * time.Second), + MetricPruningMaxRequestMetricsHistoryDays: EnvOrDefaultInt(METRIC_PRUNING_MAX_REQUEST_METRICS_HISTORY_DAYS_ENVIRONMENT_KEY, DEFAULT_METRIC_PRUNING_MAX_REQUEST_METRICS_HISTORY_DAYS), + CacheEnabled: EnvOrDefaultBool(CACHE_ENABLED_ENVIRONMENT_KEY, false), + RedisEndpointURL: os.Getenv(REDIS_ENDPOINT_URL_ENVIRONMENT_KEY), + RedisPassword: os.Getenv(REDIS_PASSWORD_ENVIRONMENT_KEY), + CacheTTL: time.Duration(EnvOrDefaultInt(CACHE_TTL_ENVIRONMENT_KEY, 0)) * time.Second, + CacheIndefinitely: EnvOrDefaultBool(CACHE_INDEFINITELY_KEY, false), + CachePrefix: os.Getenv(CACHE_PREFIX_ENVIRONMENT_KEY), + WhitelistedHeaders: parsedWhitelistedHeaders, + DefaultAccessControlAllowOriginValue: os.Getenv(DEFAULT_ACCESS_CONTROL_ALLOW_ORIGIN_VALUE_ENVIRONMENT_KEY), + HostnameToAccessControlAllowOriginValueMapRaw: rawHostnameToAccessControlAllowOriginValueMap, + HostnameToAccessControlAllowOriginValueMap: parsedHostnameToAccessControlAllowOriginValueMap, } } + +func (cfg *Config) GetAccessControlAllowOriginValue(hostname string) string { + headerValue, ok := cfg.HostnameToAccessControlAllowOriginValueMap[hostname] + if ok { + return headerValue + } + + return cfg.DefaultAccessControlAllowOriginValue +} diff --git a/config/validate.go b/config/validate.go index 73d32a9..516aa71 100644 --- a/config/validate.go +++ b/config/validate.go @@ -77,6 +77,10 @@ func Validate(config Config) error { allErrs = errors.Join(allErrs, fmt.Errorf("invalid %s specified %s, must not be empty", CACHE_PREFIX_ENVIRONMENT_KEY, config.CachePrefix)) } + if err = validateHostnameToHeaderValueMap(config.HostnameToAccessControlAllowOriginValueMapRaw, true); err != nil { + allErrs = errors.Join(allErrs, fmt.Errorf("invalid %s specified %s", HOSTNAME_TO_ACCESS_CONTROL_ALLOW_ORIGIN_VALUE_MAP_ENVIRONMENT_KEY, config.HostnameToAccessControlAllowOriginValueMapRaw), err) + } + return allErrs } @@ -89,6 +93,15 @@ func validateHostURLMap(raw string, allowEmpty bool) error { return err } +// validateHostnameToHeaderValueMap validates a raw hostname to header value map, optionally allowing the map to be empty +func validateHostnameToHeaderValueMap(raw string, allowEmpty bool) error { + _, err := ParseRawHostnameToHeaderValueMap(raw) + if allowEmpty && errors.Is(err, ErrEmptyHostnameToHeaderValueMap) { + err = nil + } + return err +} + // validateDefaultHostMapContainsHosts returns an error if there are hosts in hostMap that // are not in defaultHostMap // example: hosts in the pruning map should always have a default fallback backend diff --git a/main_test.go b/main_test.go index 74e8a6c..a05f6a4 100644 --- a/main_test.go +++ b/main_test.go @@ -31,6 +31,8 @@ import ( const ( EthClientUserAgent = "Go-http-client/1.1" + + accessControlAllowOriginHeaderName = "Access-Control-Allow-Origin" ) var ( @@ -505,6 +507,9 @@ func TestE2ETestCachingMdwWithBlockNumberParam(t *testing.T) { // check that response headers are the same equalHeaders(t, cacheMissResp.Header, cacheHitResp.Header) + + // check that CORS headers are present for cache hit scenario + require.Equal(t, cacheHitResp.Header[accessControlAllowOriginHeaderName], []string{"*"}) }) } @@ -533,6 +538,7 @@ func TestE2ETestCachingMdwWithBlockNumberParam(t *testing.T) { // equalHeaders checks that headers of headersMap1 and headersMap2 are equal // NOTE: it completely ignores presence/absence of cachemdw.CacheHeaderKey, // it's done in that way to allow comparison of headers for cache miss and cache hit cases +// also it ignores presence/absence of CORS headers func equalHeaders(t *testing.T, headersMap1, headersMap2 http.Header) { containsHeaders(t, headersMap1, headersMap2) containsHeaders(t, headersMap2, headersMap1) @@ -541,9 +547,10 @@ func equalHeaders(t *testing.T, headersMap1, headersMap2 http.Header) { // containsHeaders checks that headersMap1 contains all headers from headersMap2 and that values for headers are the same // NOTE: it completely ignores presence/absence of cachemdw.CacheHeaderKey, // it's done in that way to allow comparison of headers for cache miss and cache hit cases +// also it ignores presence/absence of CORS headers func containsHeaders(t *testing.T, headersMap1, headersMap2 http.Header) { for name, value := range headersMap1 { - if name == cachemdw.CacheHeaderKey { + if name == cachemdw.CacheHeaderKey || name == "Server" || name == accessControlAllowOriginHeaderName { continue } diff --git a/service/cachemdw/cache.go b/service/cachemdw/cache.go index 6023116..02041d5 100644 --- a/service/cachemdw/cache.go +++ b/service/cachemdw/cache.go @@ -28,6 +28,9 @@ type ServiceCache struct { cacheEnabled bool whitelistedHeaders []string + defaultAccessControlAllowOriginValue string + hostnameToAccessControlAllowOriginValueMap map[string]string + *logging.ServiceLogger } @@ -40,18 +43,22 @@ func NewServiceCache( cachePrefix string, cacheEnabled bool, whitelistedHeaders []string, + defaultAccessControlAllowOriginValue string, + hostnameToAccessControlAllowOriginValueMap map[string]string, logger *logging.ServiceLogger, ) *ServiceCache { return &ServiceCache{ - cacheClient: cacheClient, - blockGetter: blockGetter, - cacheTTL: cacheTTL, - cacheIndefinitely: cacheIndefinitely, - decodedRequestContextKey: decodedRequestContextKey, - cachePrefix: cachePrefix, - cacheEnabled: cacheEnabled, - whitelistedHeaders: whitelistedHeaders, - ServiceLogger: logger, + cacheClient: cacheClient, + blockGetter: blockGetter, + cacheTTL: cacheTTL, + cacheIndefinitely: cacheIndefinitely, + decodedRequestContextKey: decodedRequestContextKey, + cachePrefix: cachePrefix, + cacheEnabled: cacheEnabled, + whitelistedHeaders: whitelistedHeaders, + defaultAccessControlAllowOriginValue: defaultAccessControlAllowOriginValue, + hostnameToAccessControlAllowOriginValueMap: hostnameToAccessControlAllowOriginValueMap, + ServiceLogger: logger, } } diff --git a/service/cachemdw/cache_test.go b/service/cachemdw/cache_test.go index aae0807..309f808 100644 --- a/service/cachemdw/cache_test.go +++ b/service/cachemdw/cache_test.go @@ -83,6 +83,8 @@ func TestUnitTestCacheQueryResponse(t *testing.T) { defaultCachePrefixString, true, []string{}, + "*", + map[string]string{}, &logger, ) diff --git a/service/cachemdw/middleware_test.go b/service/cachemdw/middleware_test.go index 572c461..2664e9a 100644 --- a/service/cachemdw/middleware_test.go +++ b/service/cachemdw/middleware_test.go @@ -34,6 +34,8 @@ func TestUnitTestServiceCacheMiddleware(t *testing.T) { defaultCachePrefixString, true, []string{}, + "*", + map[string]string{}, &logger, ) @@ -120,6 +122,8 @@ func TestUnitTestServiceCacheMiddleware_CacheIsDisabled(t *testing.T) { defaultCachePrefixString, false, []string{}, + "*", + map[string]string{}, &logger, ) diff --git a/service/middleware.go b/service/middleware.go index e24a496..18f773c 100644 --- a/service/middleware.go +++ b/service/middleware.go @@ -254,6 +254,11 @@ func createProxyRequestMiddleware(next http.Handler, config config.Config, servi for headerName, headerValue := range typedCachedResponse.HeaderMap { w.Header().Add(headerName, headerValue) } + // add CORS headers + accessControlAllowOriginValue := config.GetAccessControlAllowOriginValue(r.Host) + if accessControlAllowOriginValue != "" { + w.Header().Add("Access-Control-Allow-Origin", accessControlAllowOriginValue) + } _, err := w.Write(typedCachedResponse.JsonRpcResponseResult) if err != nil { serviceLogger.Logger.Error().Msg(fmt.Sprintf("can't write cached response: %v", err)) diff --git a/service/service.go b/service/service.go index 0e280c5..e8a2b64 100644 --- a/service/service.go +++ b/service/service.go @@ -216,6 +216,8 @@ func createServiceCache( config.CachePrefix, config.CacheEnabled, config.WhitelistedHeaders, + config.DefaultAccessControlAllowOriginValue, + config.HostnameToAccessControlAllowOriginValueMap, logger, )