FEATURE: limit assets less that non asset paths

By default assets can be requested up to 200 times per 10 seconds
from the app, this includes CSS and avatars
This commit is contained in:
Sam 2018-03-06 15:20:39 +11:00
parent 2658ef5e0b
commit f0d5f83424
7 changed files with 81 additions and 6 deletions

View File

@ -659,6 +659,10 @@ class ApplicationController < ActionController::Base
render_to_string status: status, layout: layout, formats: [:html], template: '/exceptions/not_found'
end
def is_asset_path
request.env['DISCOURSE_IS_ASSET_PATH'] = 1
end
protected
def render_post_json(post, add_raw = true)

View File

@ -103,6 +103,8 @@ class StaticController < ApplicationController
# instead we cache the favicon in redis and serve it out real quick with
# a huge expiry, we also cache these assets in nginx so it bypassed if needed
def favicon
is_asset_path
hijack do
data = DistributedMemoizer.memoize(FAVICON + SiteSetting.favicon_url, 60 * 30) do
begin
@ -138,16 +140,22 @@ class StaticController < ApplicationController
end
def brotli_asset
is_asset_path
serve_asset(".br") do
response.headers["Content-Encoding"] = 'br'
end
end
def cdn_asset
is_asset_path
serve_asset
end
def service_worker_asset
is_asset_path
respond_to do |format|
format.js do
# https://github.com/w3c/ServiceWorker/blob/master/explainer.md#updating-a-service-worker

View File

@ -6,6 +6,8 @@ class StylesheetsController < ApplicationController
end
def show
is_asset_path
show_resource
end

View File

@ -33,6 +33,8 @@ class UserAvatarsController < ApplicationController
end
def show_proxy_letter
is_asset_path
if SiteSetting.external_system_avatars_url !~ /^\/letter_avatar_proxy/
raise Discourse::NotFound
end
@ -56,6 +58,8 @@ class UserAvatarsController < ApplicationController
end
def show_letter
is_asset_path
params.require(:username)
params.require(:version)
params.require(:size)
@ -75,6 +79,8 @@ class UserAvatarsController < ApplicationController
end
def show
is_asset_path
# we need multisite support to keep a single origin pull for CDNs
RailsMultisite::ConnectionManagement.with_hostname(params[:hostname]) do
hijack do

View File

@ -178,6 +178,10 @@ max_admin_api_reqs_per_key_per_minute = 60
max_reqs_per_ip_per_minute = 200
max_reqs_per_ip_per_10_seconds = 50
# applies to asset type routes (avatars/css and so on)
max_asset_reqs_per_ip_per_10_seconds = 200
# global rate limiter will simply warn if the limit is exceeded, can be warn+block, warn, block or none
max_reqs_per_ip_mode = none

View File

@ -171,6 +171,16 @@ class Middleware::RequestTracker
end
result
ensure
if (limiters = env['DISCOURSE_RATE_LIMITERS']) && env['DISCOURSE_IS_ASSET_PATH']
limiters.each(&:rollback!)
env['DISCOURSE_ASSET_RATE_LIMITERS'].each do |limiter|
begin
limiter.performed!
rescue RateLimiter::LimitExceeded
# skip
end
end
end
log_request_info(env, result, info) unless env["discourse.request_tracker.skip"]
end
@ -213,16 +223,35 @@ class Middleware::RequestTracker
global: true
)
limiter_assets10 = RateLimiter.new(
nil,
"global_ip_limit_10_assets_#{ip}",
GlobalSetting.max_asset_reqs_per_ip_per_10_seconds,
10,
global: true
)
env['DISCOURSE_RATE_LIMITERS'] = [limiter10, limiter60]
env['DISCOURSE_ASSET_RATE_LIMITERS'] = [limiter_assets10]
warn = GlobalSetting.max_reqs_per_ip_mode == "warn" ||
GlobalSetting.max_reqs_per_ip_mode == "warn+block"
if !limiter_assets10.can_perform?
if warn
Rails.logger.warn("Global asset IP rate limit exceeded for #{ip}: 10 second rate limit, uri: #{env["REQUEST_URI"]}")
end
return !(GlobalSetting.max_reqs_per_ip_mode == "warn")
end
type = 10
begin
limiter10.performed!
type = 60
limiter60.performed!
rescue RateLimiter::LimitExceeded
if (
GlobalSetting.max_reqs_per_ip_mode == "warn" ||
GlobalSetting.max_reqs_per_ip_mode == "warn+block"
)
if warn
Rails.logger.warn("Global IP rate limit exceeded for #{ip}: #{type} second rate limit, uri: #{env["REQUEST_URI"]}")
!(GlobalSetting.max_reqs_per_ip_mode == "warn")
else

View File

@ -191,6 +191,28 @@ describe Middleware::RequestTracker do
expect(status).to eq(200)
end
it "allows assets for more requests" do
global_setting :max_reqs_per_ip_per_10_seconds, 1
global_setting :max_reqs_per_ip_mode, 'block'
global_setting :max_asset_reqs_per_ip_per_10_seconds, 3
env1 = env("REMOTE_ADDR" => "1.1.1.1", "DISCOURSE_IS_ASSET_PATH" => 1)
status, _ = middleware.call(env1)
expect(status).to eq(200)
status, _ = middleware.call(env1)
expect(status).to eq(200)
status, _ = middleware.call(env1)
expect(status).to eq(200)
status, _ = middleware.call(env1)
expect(status).to eq(429)
env2 = env("REMOTE_ADDR" => "1.1.1.1")
status, _ = middleware.call(env2)
expect(status).to eq(429)
end
it "does block if rate limiter is enabled" do
global_setting :max_reqs_per_ip_per_10_seconds, 1
global_setting :max_reqs_per_ip_mode, 'block'
@ -199,13 +221,13 @@ describe Middleware::RequestTracker do
env2 = env("REMOTE_ADDR" => "1.1.1.2")
status, _ = middleware.call(env1)
status, _ = middleware.call(env1)
expect(status).to eq(200)
status, _ = middleware.call(env1)
expect(status).to eq(429)
status, _ = middleware.call(env2)
expect(status).to eq(200)
end
end