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) {
|
buildBuffer(buffer) {
|
||||||
let notices = [];
|
let notices = [];
|
||||||
|
|
||||||
|
if ($.cookie("dosp") === "1") {
|
||||||
|
$.cookie("dosp", null, { path: '/' });
|
||||||
|
notices.push([I18n.t("forced_anonymous"), 'forced-anonymous']);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.session.get('safe_mode')) {
|
if (this.session.get('safe_mode')) {
|
||||||
notices.push([I18n.t("safe_mode.enabled"), '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
|
# bypass rate limiting any IP resolved as a private IP
|
||||||
max_reqs_rate_limit_on_private = false
|
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_forum: "Hey, you should join this forum!"
|
||||||
custom_message_template_topic: "Hey, I thought you might enjoy this topic!"
|
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:
|
safe_mode:
|
||||||
enabled: "Safe mode is enabled, to exit safe mode close this browser window"
|
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 "mobile_detection"
|
||||||
require_dependency "crawler_detection"
|
require_dependency "crawler_detection"
|
||||||
require_dependency "guardian"
|
require_dependency "guardian"
|
||||||
|
@ -10,9 +12,9 @@ module Middleware
|
||||||
end
|
end
|
||||||
|
|
||||||
class Helper
|
class Helper
|
||||||
USER_AGENT = "HTTP_USER_AGENT".freeze
|
USER_AGENT = "HTTP_USER_AGENT"
|
||||||
RACK_SESSION = "rack.session".freeze
|
RACK_SESSION = "rack.session"
|
||||||
ACCEPT_ENCODING = "HTTP_ACCEPT_ENCODING".freeze
|
ACCEPT_ENCODING = "HTTP_ACCEPT_ENCODING"
|
||||||
|
|
||||||
def initialize(env)
|
def initialize(env)
|
||||||
@env = env
|
@env = env
|
||||||
|
@ -86,7 +88,40 @@ module Middleware
|
||||||
|
|
||||||
def no_cache_bypass
|
def no_cache_bypass
|
||||||
request = Rack::Request.new(@env)
|
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
|
end
|
||||||
|
|
||||||
def cacheable?
|
def cacheable?
|
||||||
|
@ -142,13 +177,26 @@ module Middleware
|
||||||
|
|
||||||
def call(env)
|
def call(env)
|
||||||
helper = Helper.new(env)
|
helper = Helper.new(env)
|
||||||
|
force_anon = false
|
||||||
|
|
||||||
if helper.cacheable?
|
if helper.should_force_anonymous?
|
||||||
helper.cached || helper.cache(@app.call(env))
|
force_anon = env["DISCOURSE_FORCE_ANON"] = true
|
||||||
else
|
helper.force_anonymous!
|
||||||
@app.call(env)
|
|
||||||
end
|
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
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -80,14 +80,17 @@ class RateLimiter
|
||||||
PERFORM_LUA_SHA = Digest::SHA1.hexdigest(PERFORM_LUA)
|
PERFORM_LUA_SHA = Digest::SHA1.hexdigest(PERFORM_LUA)
|
||||||
end
|
end
|
||||||
|
|
||||||
def performed!
|
def performed!(raise_error: true)
|
||||||
return if rate_unlimited?
|
return if rate_unlimited?
|
||||||
now = Time.now.to_i
|
now = Time.now.to_i
|
||||||
|
|
||||||
if ((max || 0) <= 0) ||
|
if ((max || 0) <= 0) ||
|
||||||
(eval_lua(PERFORM_LUA, PERFORM_LUA_SHA, [prefixed_key], [now, @secs, @max]) == 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
|
end
|
||||||
rescue Redis::CommandError => e
|
rescue Redis::CommandError => e
|
||||||
if e.message =~ /READONLY/
|
if e.message =~ /READONLY/
|
||||||
|
|
|
@ -45,6 +45,55 @@ describe Middleware::AnonymousCache::Helper do
|
||||||
end
|
end
|
||||||
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
|
context "cached" do
|
||||||
let!(:helper) do
|
let!(:helper) do
|
||||||
new_helper("ANON_CACHE_DURATION" => 10)
|
new_helper("ANON_CACHE_DURATION" => 10)
|
||||||
|
|
Loading…
Reference in New Issue