Skip to content

Commit

Permalink
Merge pull request #599 from tablelandnetwork/bcalza/ratelim
Browse files Browse the repository at this point in the history
allows authorized clients to bypass rate limiter
  • Loading branch information
brunocalza authored Aug 15, 2023
2 parents 379b0fd + 26b9025 commit 0b122ac
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 32 deletions.
1 change: 1 addition & 0 deletions cmd/api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type HTTPConfig struct {

RateLimInterval string `default:"1s"`
MaxRequestPerInterval uint64 `default:"10"`
APIKey string `default:""` // if client passes the key it will not be affected by rate limiter
}

// GatewayConfig contains configuration for the Gateway.
Expand Down
1 change: 1 addition & 0 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ func createAPIServer(
httpConfig.MaxRequestPerInterval,
rateLimInterval,
supportedChainIDs,
httpConfig.APIKey,
)
if err != nil {
return nil, fmt.Errorf("configuring router: %s", err)
Expand Down
1 change: 1 addition & 0 deletions docker/deployed/mainnet/api/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"Port": "8080",
"RateLimInterval": "1s",
"MaxRequestPerInterval": 10,
"ApiKey" : "${HTTP_RATE_LIMITER_API_KEY}",
"TLSCert": "${VALIDATOR_TLS_CERT}",
"TLSKey": "${VALIDATOR_TLS_KEY}"
},
Expand Down
1 change: 1 addition & 0 deletions docker/deployed/staging/api/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"Port": "8080",
"RateLimInterval": "1s",
"MaxRequestPerInterval": 10,
"ApiKey" : "${HTTP_RATE_LIMITER_API_KEY}",
"TLSCert": "${VALIDATOR_TLS_CERT}",
"TLSKey": "${VALIDATOR_TLS_KEY}"
},
Expand Down
20 changes: 1 addition & 19 deletions docker/deployed/testnet/api/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"Port": "8080",
"RateLimInterval": "1s",
"MaxRequestPerInterval": 10,
"ApiKey" : "${HTTP_RATE_LIMITER_API_KEY}",
"TLSCert": "${VALIDATOR_TLS_CERT}",
"TLSKey": "${VALIDATOR_TLS_KEY}"
},
Expand Down Expand Up @@ -51,25 +52,6 @@
"ChainStackCollectFrequency": "15m"
},
"Chains": [
{
"Name": "Ethereum Goerli",
"ChainID": 5,
"Registry": {
"EthEndpoint": "wss://eth-goerli.alchemyapi.io/v2/${VALIDATOR_ALCHEMY_ETHEREUM_GOERLI_API_KEY}",
"ContractAddress": "0xDA8EA22d092307874f30A1F277D1388dca0BA97a"
},
"EventFeed": {
"ChainAPIBackoff": "15s",
"NewBlockPollFreq": "10s",
"MinBlockDepth": 1,
"PersistEvents": true
},
"EventProcessor": {
"BlockFailedExecutionBackoff": "10s",
"DedupExecutedTxns": true
},
"HashCalculationStep": 150
},
{
"Name": "Ethereum Sepolia",
"ChainID": 11155111,
Expand Down
75 changes: 69 additions & 6 deletions internal/router/middlewares/ratelim.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"fmt"
"net"
"net/http"
"strconv"
"strings"
"time"

"github.com/gorilla/mux"
"github.com/sethvargo/go-limiter"
"github.com/sethvargo/go-limiter/httplimit"
"github.com/sethvargo/go-limiter/memorystore"
)
Expand All @@ -22,6 +24,7 @@ type RateLimiterConfig struct {
type RateLimiterRouteConfig struct {
MaxRPI uint64
Interval time.Duration
APIKey string
}

// RateLimitController creates a new middleware to rate limit requests.
Expand All @@ -47,19 +50,20 @@ func RateLimitController(cfg RateLimiterConfig) (mux.MiddlewareFunc, error) {
}, nil
}

func createRateLimiter(cfg RateLimiterRouteConfig, kf httplimit.KeyFunc) (*httplimit.Middleware, error) {
func createRateLimiter(cfg RateLimiterRouteConfig, kf httplimit.KeyFunc) (*middleware, error) {
defaultStore, err := memorystore.New(&memorystore.Config{
Tokens: cfg.MaxRPI,
Interval: cfg.Interval,
})
if err != nil {
return nil, fmt.Errorf("creating default memory: %s", err)
}
m, err := httplimit.NewMiddleware(defaultStore, kf)
if err != nil {
return nil, fmt.Errorf("creating default httplimiter: %s", err)
}
return m, nil

return &middleware{
store: defaultStore,
keyFunc: kf,
apiKey: cfg.APIKey,
}, nil
}

func extractClientIP(r *http.Request) (string, error) {
Expand All @@ -77,3 +81,62 @@ func extractClientIP(r *http.Request) (string, error) {
}
return ip, nil
}

type middleware struct {
store limiter.Store
keyFunc httplimit.KeyFunc

// clients with key are not affected by rate limiter
apiKey string
}

// Handle returns the HTTP handler as a middleware. This handler calls Take() on
// the store and sets the common rate limiting headers. If the take is
// successful, the remaining middleware is called. If take is unsuccessful, the
// middleware chain is halted and the function renders a 429 to the caller with
// metadata about when it's safe to retry.
func (m *middleware) Handle(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

// Call the key function - if this fails, it's an internal server error.
key, err := m.keyFunc(r)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}

// skip rate limiting checks if api key is provided
if key := r.Header.Get("Api-Key"); key != "" && m.apiKey != "" {
if strings.EqualFold(key, m.apiKey) {
next.ServeHTTP(w, r)
return
}
}

// Take from the store.
limit, remaining, reset, ok, err := m.store.Take(ctx, key)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}

resetTime := time.Unix(0, int64(reset)).UTC().Format(time.RFC1123)

// Set headers (we do this regardless of whether the request is permitted).
w.Header().Set("X-RateLimit-Limit", strconv.FormatUint(limit, 10))
w.Header().Set("X-RateLimit-Remaining", strconv.FormatUint(remaining, 10))
w.Header().Set("X-RateLimit-Reset", resetTime)

// Fail if there were no tokens remaining.
if !ok {
w.Header().Set("Retry-After", resetTime)
http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
return
}

// If we got this far, we're allowed to continue, so call the next middleware
// in the stack to continue processing.
next.ServeHTTP(w, r)
})
}
23 changes: 17 additions & 6 deletions internal/router/middlewares/ratelim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func TestLimit1IP(t *testing.T) {
callRPS int
limitRPS int
forwardedFor bool
allow bool
}

tests := []testCase{
Expand All @@ -28,6 +29,9 @@ func TestLimit1IP(t *testing.T) {

{name: "success", callRPS: 100, limitRPS: 500, forwardedFor: false},
{name: "block-me", callRPS: 1000, limitRPS: 500, forwardedFor: false},

{name: "allow-me", callRPS: 1000, limitRPS: 500, forwardedFor: false, allow: true},
{name: "forwarded-allow-me", callRPS: 1000, limitRPS: 500, forwardedFor: true, allow: true},
}

for _, tc := range tests {
Expand All @@ -41,28 +45,35 @@ func TestLimit1IP(t *testing.T) {
Interval: time.Second,
},
}
rlcm, err := RateLimitController(cfg)
require.NoError(t, err)
rlc := rlcm(dummyHandler{})

ctx := context.Background()
r, err := http.NewRequestWithContext(ctx, "", "", nil)
require.NoError(t, err)

ip := uuid.NewString()
if tc.forwardedFor {
r.Header.Set("X-Forwarded-For", uuid.NewString())
r.Header.Set("X-Forwarded-For", ip)
} else {
r.RemoteAddr = uuid.NewString() + ":1234"
r.RemoteAddr = ip + ":1234"
}

if tc.allow {
r.Header.Set("Api-Key", "MYSECRETKEY")
cfg.Default.APIKey = "MYSECRETKEY"
}

rlcm, err := RateLimitController(cfg)
require.NoError(t, err)
rlc := rlcm(dummyHandler{})

res := httptest.NewRecorder()

// Verify that after some seconds making requests with the configured
// callRPS with the limitRPS, we are getting the expected output:
// - If callRPS < limitRPS, we never get a 429.
// - If callRPS > limitRPS, we eventually should see a 429.
assertFunc := require.Eventually
if tc.callRPS < tc.limitRPS {
if tc.callRPS < tc.limitRPS || tc.allow {
assertFunc = require.Never
}
assertFunc(t, func() bool {
Expand Down
2 changes: 2 additions & 0 deletions internal/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func ConfiguredRouter(
maxRPI uint64,
rateLimInterval time.Duration,
supportedChainIDs []tableland.ChainID,
apiKey string,
) (*Router, error) {
// General router configuration.
router := newRouter()
Expand All @@ -28,6 +29,7 @@ func ConfiguredRouter(
Default: middlewares.RateLimiterRouteConfig{
MaxRPI: maxRPI,
Interval: rateLimInterval,
APIKey: apiKey,
},
}
rateLim, err := middlewares.RateLimitController(cfg)
Expand Down
2 changes: 1 addition & 1 deletion tests/fullstack/fullstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func CreateFullStack(t *testing.T, deps Deps) FullStack {
require.NoError(t, err)
}

router, err := router.ConfiguredRouter(gatewayService, 10, time.Second, []tableland.ChainID{ChainID})
router, err := router.ConfiguredRouter(gatewayService, 10, time.Second, []tableland.ChainID{ChainID}, "")
require.NoError(t, err)

server := httptest.NewServer(router.Handler())
Expand Down

0 comments on commit 0b122ac

Please sign in to comment.