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' render_to_string status: status, layout: layout, formats: [:html], template: '/exceptions/not_found'
end end
def is_asset_path
request.env['DISCOURSE_IS_ASSET_PATH'] = 1
end
protected protected
def render_post_json(post, add_raw = true) 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 # 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 # a huge expiry, we also cache these assets in nginx so it bypassed if needed
def favicon def favicon
is_asset_path
hijack do hijack do
data = DistributedMemoizer.memoize(FAVICON + SiteSetting.favicon_url, 60 * 30) do data = DistributedMemoizer.memoize(FAVICON + SiteSetting.favicon_url, 60 * 30) do
begin begin
@ -138,16 +140,22 @@ class StaticController < ApplicationController
end end
def brotli_asset def brotli_asset
is_asset_path
serve_asset(".br") do serve_asset(".br") do
response.headers["Content-Encoding"] = 'br' response.headers["Content-Encoding"] = 'br'
end end
end end
def cdn_asset def cdn_asset
is_asset_path
serve_asset serve_asset
end end
def service_worker_asset def service_worker_asset
is_asset_path
respond_to do |format| respond_to do |format|
format.js do format.js do
# https://github.com/w3c/ServiceWorker/blob/master/explainer.md#updating-a-service-worker # https://github.com/w3c/ServiceWorker/blob/master/explainer.md#updating-a-service-worker

View File

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

View File

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

View File

@ -171,6 +171,16 @@ class Middleware::RequestTracker
end end
result result
ensure 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"] log_request_info(env, result, info) unless env["discourse.request_tracker.skip"]
end end
@ -213,16 +223,35 @@ class Middleware::RequestTracker
global: true 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 type = 10
begin begin
limiter10.performed! limiter10.performed!
type = 60 type = 60
limiter60.performed! limiter60.performed!
rescue RateLimiter::LimitExceeded rescue RateLimiter::LimitExceeded
if ( if warn
GlobalSetting.max_reqs_per_ip_mode == "warn" ||
GlobalSetting.max_reqs_per_ip_mode == "warn+block"
)
Rails.logger.warn("Global IP rate limit exceeded for #{ip}: #{type} second rate limit, uri: #{env["REQUEST_URI"]}") 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") !(GlobalSetting.max_reqs_per_ip_mode == "warn")
else else

View File

@ -191,6 +191,28 @@ describe Middleware::RequestTracker do
expect(status).to eq(200) expect(status).to eq(200)
end 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 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_per_10_seconds, 1
global_setting :max_reqs_per_ip_mode, 'block' global_setting :max_reqs_per_ip_mode, 'block'
@ -199,13 +221,13 @@ describe Middleware::RequestTracker do
env2 = env("REMOTE_ADDR" => "1.1.1.2") env2 = env("REMOTE_ADDR" => "1.1.1.2")
status, _ = middleware.call(env1) status, _ = middleware.call(env1)
status, _ = middleware.call(env1) expect(status).to eq(200)
status, _ = middleware.call(env1)
expect(status).to eq(429) expect(status).to eq(429)
status, _ = middleware.call(env2) status, _ = middleware.call(env2)
expect(status).to eq(200) expect(status).to eq(200)
end end
end end