Skip to content

Commit

Permalink
V2 Rate Limiter: improve code formatting
Browse files Browse the repository at this point in the history
- Replace 'skip_rate_limiting' with 'apply_rate_limiting'
- Code cleanup
- Improve signatures & code structure
- Use shared test examples
- Fix admin test
  • Loading branch information
johha committed Jul 26, 2022
1 parent efdd0d4 commit 954a4f8
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 205 deletions.
2 changes: 1 addition & 1 deletion lib/cloud_controller/config_schemas/base/api_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ class ApiSchema < VCAP::Config
max_concurrent_service_broker_requests: Integer,
shared_isolation_segment_name: String,

rate_limiter_v2_api: {
optional(:rate_limiter_v2_api) => {
enabled: bool,
per_process_general_limit: Integer,
global_general_limit: Integer,
Expand Down
39 changes: 21 additions & 18 deletions middleware/base_rate_limiter.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
require 'mixins/client_ip'
require 'mixins/user_reset_interval'

module CloudFoundry
module Middleware
RequestCount = Struct.new(:requests, :valid_until)

class BaseRequestCounter
class RequestCounter
include CloudFoundry::Middleware::UserResetInterval

def initialize
reset
end
RequestCount = Struct.new(:requests, :valid_until)

# needed for testing
def reset
def initialize
@mutex = Mutex.new
@data = {}
end
Expand Down Expand Up @@ -58,7 +56,7 @@ def initialize(suffix)
@remaining = nil
end

def as_hash
def to_hash
return {} if [@limit, @reset, @remaining].all?(&:nil?)

{ "#{@prefix}-Limit#{@suffix}" => @limit, "#{@prefix}-Reset#{@suffix}" => @reset, "#{@prefix}-Remaining#{@suffix}" => @remaining }
Expand All @@ -81,7 +79,7 @@ def call(env)

request = ActionDispatch::Request.new(env)

unless skip_rate_limiting?(env, request)
if apply_rate_limiting?(env)
user_guid = user_token?(env) ? env['cf.user_guid'] : client_ip(request)

count, valid_until = @request_counter.get(user_guid, @reset_interval, @logger)
Expand All @@ -97,21 +95,25 @@ def call(env)
end

status, headers, body = @app.call(env)
[status, headers.merge(rate_limit_headers.as_hash), body]
[status, headers.merge(rate_limit_headers.to_hash), body]
end

private

def skip_rate_limiting?(env, request)
raise NotImplementedError
def apply_rate_limiting?(env)
raise 'method should be implemented in concrete class'
end

def global_request_limit(env)
raise NotImplementedError
raise 'method should be implemented in concrete class'
end

def rate_limit_error(env)
raise NotImplementedError
raise 'method should be implemented in concrete class'
end

def per_process_request_limit(env)
raise 'method should be implemented in concrete class'
end

def exceeded_rate_limit(count, env)
Expand All @@ -131,8 +133,9 @@ def user_token?(env)
!!env['cf.user_guid']
end

def basic_auth?(auth)
(auth.provided? && auth.basic?)
def basic_auth?(env)
auth = Rack::Auth::Basic::Request.new(env)
auth.provided? && auth.basic?
end

def admin?
Expand All @@ -145,7 +148,7 @@ def too_many_requests!(env, rate_limit_headers)
headers['Content-Type'] = 'text/plain; charset=utf-8'
message = rate_limit_error(env).to_json
headers['Content-Length'] = message.length.to_s
[429, rate_limit_headers.as_hash.merge(headers), [message]]
[429, rate_limit_headers.to_hash.merge(headers), [message]]
end
end
end
Expand Down
12 changes: 5 additions & 7 deletions middleware/rate_limiter.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
require 'mixins/client_ip'
require 'mixins/user_reset_interval'
require 'base_rate_limiter'

module CloudFoundry
module Middleware
REQUEST_COUNTER = BaseRequestCounter.new

class RateLimiter < BaseRateLimiter
REQUEST_COUNTER = RequestCounter.new

def initialize(app, opts)
@per_process_general_limit = opts[:per_process_general_limit]
@global_general_limit = opts[:global_general_limit]
Expand All @@ -17,9 +15,9 @@ def initialize(app, opts)

private

def skip_rate_limiting?(env, request)
auth = Rack::Auth::Basic::Request.new(env)
basic_auth?(auth) || internal_api?(request) || root_api?(request) || admin?
def apply_rate_limiting?(env)
request = ActionDispatch::Request.new(env)
!basic_auth?(env) && !internal_api?(request) && !root_api?(request) && !admin?
end

def root_api?(request)
Expand Down
21 changes: 11 additions & 10 deletions middleware/rate_limiter_v2_api.rb
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
require 'mixins/client_ip'
require 'mixins/user_reset_interval'
require 'base_rate_limiter'

module CloudFoundry
module Middleware
REQUEST_COUNTER_V2_API = BaseRequestCounter.new

class RateLimiterV2API < BaseRateLimiter
REQUEST_COUNTER = RequestCounter.new

def initialize(app, opts)
@per_process_general_limit = opts[:per_process_general_limit]
@global_general_limit = opts[:global_general_limit]
@per_process_admin_limit = opts[:per_process_admin_limit]
@global_admin_limit = opts[:global_admin_limit]
super(app, opts[:logger], REQUEST_COUNTER_V2_API, opts[:interval], 'V2-API')
super(app, opts[:logger], REQUEST_COUNTER, opts[:interval], 'V2-API')
end

private

def skip_rate_limiting?(env, request)
auth = Rack::Auth::Basic::Request.new(env)
basic_auth?(auth) || !request.fullpath.match(%r{\A/v2/(?!(info)).+})
def apply_rate_limiting?(env)
!basic_auth?(env) && v2_api?(env)
end

def v2_api?(env)
request = ActionDispatch::Request.new(env)
request.fullpath.match(%r{\A/v2/(?!(info)).+})
end

def global_request_limit(env)
Expand All @@ -31,8 +33,7 @@ def per_process_request_limit(env)
end

def rate_limit_error(env)
error_name = 'RateLimitV2APIExceeded'
api_error = CloudController::Errors::ApiError.new_from_details(error_name)
api_error = CloudController::Errors::ApiError.new_from_details('RateLimitV2APIExceeded')
ErrorPresenter.new(api_error, Rails.env.test?, V2ErrorHasher.new(api_error)).to_hash
end
end
Expand Down
11 changes: 11 additions & 0 deletions spec/support/shared_examples/middleware/rate_limiting.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
RSpec.shared_examples_for 'endpoint exempts from rate limiting' do |header_suffix=''|
it 'exempts them from rate limiting' do
allow(ActionDispatch::Request).to receive(:new).and_return(fake_request)
_, response_headers, _ = middleware.call(env)
expect(request_counter).not_to have_received(:get)
expect(request_counter).not_to have_received(:increment)
expect(response_headers["X-RateLimit-Limit#{header_suffix}"]).to be_nil
expect(response_headers["X-RateLimit-Remaining#{header_suffix}"]).to be_nil
expect(response_headers["X-RateLimit-Reset#{header_suffix}"]).to be_nil
end
end
Loading

0 comments on commit 954a4f8

Please sign in to comment.