FEATURE: if site is under extreme load show anon view
If a particular path is being hit extremely hard by logged on users, revert to anonymous cached view. This will only come into effect if 3 requests queue for longer than 2 seconds on a *single* path. This can happen if a URL is shared with the entire forum base and everyone is logged on
This commit is contained in:
parent
7bf9650e96
commit
59cd7894d9
|
@ -9,6 +9,11 @@ export default Ember.Component.extend(bufferedRender({
|
|||
buildBuffer(buffer) {
|
||||
let notices = [];
|
||||
|
||||
if ($.cookie("dosp") === "1") {
|
||||
$.cookie("dosp", null, { path: '/' });
|
||||
notices.push([I18n.t("forced_anonymous"), 'forced-anonymous']);
|
||||
}
|
||||
|
||||
if (this.session.get('safe_mode')) {
|
||||
notices.push([I18n.t("safe_mode.enabled"), 'safe-mode']);
|
||||
}
|
||||
|
|
|
@ -194,3 +194,11 @@ max_reqs_per_ip_mode = none
|
|||
|
||||
# bypass rate limiting any IP resolved as a private IP
|
||||
max_reqs_rate_limit_on_private = false
|
||||
|
||||
# logged in DoS protection
|
||||
|
||||
# protection will only trigger for requests that queue longer than this amount
|
||||
force_anonymous_min_queue_seconds = 2
|
||||
# only trigger anon if we see more than N requests for this path in last 10 seconds
|
||||
force_anonymous_min_per_10_seconds = 3
|
||||
|
||||
|
|
|
@ -2690,6 +2690,8 @@ en:
|
|||
custom_message_template_forum: "Hey, you should join this forum!"
|
||||
custom_message_template_topic: "Hey, I thought you might enjoy this topic!"
|
||||
|
||||
forced_anonymous: "Site is under heavy load, we are temporarily presenting you with a cached anonymous view"
|
||||
|
||||
safe_mode:
|
||||
enabled: "Safe mode is enabled, to exit safe mode close this browser window"
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_dependency "mobile_detection"
|
||||
require_dependency "crawler_detection"
|
||||
require_dependency "guardian"
|
||||
|
@ -10,9 +12,9 @@ module Middleware
|
|||
end
|
||||
|
||||
class Helper
|
||||
USER_AGENT = "HTTP_USER_AGENT".freeze
|
||||
RACK_SESSION = "rack.session".freeze
|
||||
ACCEPT_ENCODING = "HTTP_ACCEPT_ENCODING".freeze
|
||||
USER_AGENT = "HTTP_USER_AGENT"
|
||||
RACK_SESSION = "rack.session"
|
||||
ACCEPT_ENCODING = "HTTP_ACCEPT_ENCODING"
|
||||
|
||||
def initialize(env)
|
||||
@env = env
|
||||
|
@ -86,7 +88,40 @@ module Middleware
|
|||
|
||||
def no_cache_bypass
|
||||
request = Rack::Request.new(@env)
|
||||
request.cookies['_bypass_cache'].nil?
|
||||
request.cookies['_bypass_cache'].nil? &&
|
||||
request[Auth::DefaultCurrentUserProvider::API_KEY].nil? &&
|
||||
@env[Auth::DefaultCurrentUserProvider::USER_API_KEY].nil?
|
||||
end
|
||||
|
||||
def force_anonymous!
|
||||
@env[Auth::DefaultCurrentUserProvider::USER_API_KEY] = nil
|
||||
@env['HTTP_COOKIE'] = nil
|
||||
@env['rack.request.cookie.hash'] = {}
|
||||
@env['rack.request.cookie.string'] = ''
|
||||
@env['_bypass_cache'] = nil
|
||||
request = Rack::Request.new(@env)
|
||||
request.delete_param('api_username')
|
||||
request.delete_param('api_key')
|
||||
end
|
||||
|
||||
def check_logged_in_rate_limit!
|
||||
limiter = RateLimiter.new(
|
||||
nil,
|
||||
"logged_in_anon_cache_#{@env["HOST"]}/#{@env["REQUEST_URI"]}",
|
||||
GlobalSetting.force_anonymous_min_per_10_seconds,
|
||||
10
|
||||
)
|
||||
!limiter.performed!(raise_error: false)
|
||||
end
|
||||
|
||||
def should_force_anonymous?
|
||||
if queue_time = @env['REQUEST_QUEUE_SECONDS']
|
||||
if queue_time > GlobalSetting.force_anonymous_min_queue_seconds && get?
|
||||
return check_logged_in_rate_limit!
|
||||
end
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def cacheable?
|
||||
|
@ -142,13 +177,26 @@ module Middleware
|
|||
|
||||
def call(env)
|
||||
helper = Helper.new(env)
|
||||
force_anon = false
|
||||
|
||||
if helper.cacheable?
|
||||
helper.cached || helper.cache(@app.call(env))
|
||||
else
|
||||
@app.call(env)
|
||||
if helper.should_force_anonymous?
|
||||
force_anon = env["DISCOURSE_FORCE_ANON"] = true
|
||||
helper.force_anonymous!
|
||||
end
|
||||
|
||||
result =
|
||||
if helper.cacheable?
|
||||
helper.cached || helper.cache(@app.call(env))
|
||||
else
|
||||
@app.call(env)
|
||||
end
|
||||
|
||||
if force_anon
|
||||
result[1]["Set-Cookie"] = "dosp=1"
|
||||
end
|
||||
|
||||
result
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -80,14 +80,17 @@ class RateLimiter
|
|||
PERFORM_LUA_SHA = Digest::SHA1.hexdigest(PERFORM_LUA)
|
||||
end
|
||||
|
||||
def performed!
|
||||
def performed!(raise_error: true)
|
||||
return if rate_unlimited?
|
||||
now = Time.now.to_i
|
||||
|
||||
if ((max || 0) <= 0) ||
|
||||
(eval_lua(PERFORM_LUA, PERFORM_LUA_SHA, [prefixed_key], [now, @secs, @max]) == 0)
|
||||
|
||||
raise RateLimiter::LimitExceeded.new(seconds_to_wait, @type)
|
||||
raise RateLimiter::LimitExceeded.new(seconds_to_wait, @type) if raise_error
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
rescue Redis::CommandError => e
|
||||
if e.message =~ /READONLY/
|
||||
|
|
|
@ -45,6 +45,55 @@ describe Middleware::AnonymousCache::Helper do
|
|||
end
|
||||
end
|
||||
|
||||
context 'force_anonymous!' do
|
||||
before do
|
||||
RateLimiter.enable
|
||||
end
|
||||
|
||||
after do
|
||||
RateLimiter.disable
|
||||
end
|
||||
|
||||
it 'will revert to anonymous once we reach the limit' do
|
||||
|
||||
RateLimiter.clear_all!
|
||||
|
||||
is_anon = false
|
||||
|
||||
app = Middleware::AnonymousCache.new(
|
||||
lambda do |env|
|
||||
is_anon = env["HTTP_COOKIE"].nil?
|
||||
[200, {}, ["ok"]]
|
||||
end
|
||||
)
|
||||
|
||||
global_setting :force_anonymous_min_per_10_seconds, 2
|
||||
global_setting :force_anonymous_min_queue_seconds, 1
|
||||
|
||||
env = {
|
||||
"HTTP_COOKIE" => "_t=#{SecureRandom.hex}",
|
||||
"HOST" => "site.com",
|
||||
"REQUEST_METHOD" => "GET",
|
||||
"REQUEST_URI" => "/somewhere/rainbow",
|
||||
"REQUEST_QUEUE_SECONDS" => 2.1,
|
||||
"rack.input" => StringIO.new
|
||||
}
|
||||
|
||||
app.call(env)
|
||||
expect(is_anon).to eq(false)
|
||||
|
||||
app.call(env)
|
||||
expect(is_anon).to eq(false)
|
||||
|
||||
app.call(env)
|
||||
expect(is_anon).to eq(true)
|
||||
|
||||
_status, headers, _body = app.call(env)
|
||||
expect(is_anon).to eq(true)
|
||||
expect(headers['Set-Cookie']).to eq('dosp=1')
|
||||
end
|
||||
end
|
||||
|
||||
context "cached" do
|
||||
let!(:helper) do
|
||||
new_helper("ANON_CACHE_DURATION" => 10)
|
||||
|
|
Loading…
Reference in New Issue