2019-04-29 20:27:42 -04:00
# frozen_string_literal: true
2015-10-11 05:41:23 -04:00
require " rails_helper "
2015-02-05 00:08:52 -05:00
describe Middleware :: RequestTracker do
def env ( opts = { } )
{
" HTTP_HOST " = > " http://test.com " ,
2018-01-16 00:28:11 -05:00
" HTTP_USER_AGENT " = > " Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36 " ,
2015-02-05 00:08:52 -05:00
" REQUEST_URI " = > " /path?bla=1 " ,
" REQUEST_METHOD " = > " GET " ,
2018-03-22 17:57:44 -04:00
" HTTP_ACCEPT " = > " text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 " ,
2015-02-05 00:08:52 -05:00
" rack.input " = > " "
} . merge ( opts )
end
2020-05-18 05:22:39 -04:00
before do
2021-03-23 15:32:36 -04:00
ApplicationRequest . clear_cache!
2020-05-18 05:22:39 -04:00
ApplicationRequest . enable
end
after do
ApplicationRequest . disable
ApplicationRequest . clear_cache!
end
2019-12-09 01:43:51 -05:00
context " full request " do
before do
@orig = WebCrawlerRequest . autoflush
WebCrawlerRequest . autoflush = 1
end
after do
WebCrawlerRequest . autoflush = @orig
end
it " can handle rogue user agents " do
agent = ( + " Evil Googlebot String \xc3 \x28 " ) . force_encoding ( " Windows-1252 " )
middleware = Middleware :: RequestTracker . new ( - > ( env ) { [ " 200 " , { " Content-Type " = > " text/html " } , [ " " ] ] } )
middleware . call ( env ( " HTTP_USER_AGENT " = > agent ) )
expect ( WebCrawlerRequest . where ( user_agent : agent . encode ( 'utf-8' ) ) . count ) . to eq ( 1 )
end
end
2015-02-05 00:08:52 -05:00
context " log_request " do
2015-02-25 19:40:57 -05:00
before do
2015-02-05 00:08:52 -05:00
freeze_time Time . now
ApplicationRequest . clear_cache!
2015-02-25 19:40:57 -05:00
end
def log_tracked_view ( val )
data = Middleware :: RequestTracker . get_data ( env (
" HTTP_DISCOURSE_TRACK_VIEW " = > val
2017-10-17 21:10:12 -04:00
) , [ " 200 " , { " Content-Type " = > 'text/html' } ] , 0 . 2 )
2015-02-25 19:40:57 -05:00
Middleware :: RequestTracker . log_request ( data )
end
it " can exclude/include based on custom header " do
log_tracked_view ( " true " )
log_tracked_view ( " 1 " )
log_tracked_view ( " false " )
log_tracked_view ( " 0 " )
ApplicationRequest . write_cache!
2015-04-25 11:18:35 -04:00
expect ( ApplicationRequest . page_view_anon . first . count ) . to eq ( 2 )
2015-02-25 19:40:57 -05:00
end
it " can log requests correctly " do
2015-02-10 01:03:33 -05:00
data = Middleware :: RequestTracker . get_data ( env (
2015-02-05 22:39:04 -05:00
" HTTP_USER_AGENT " = > " AdsBot-Google (+http://www.google.com/adsbot.html) "
2017-10-17 21:10:12 -04:00
) , [ " 200 " , { " Content-Type " = > 'text/html' } ] , 0 . 1 )
2015-02-10 01:03:33 -05:00
Middleware :: RequestTracker . log_request ( data )
data = Middleware :: RequestTracker . get_data ( env (
2015-02-05 22:39:04 -05:00
" HTTP_DISCOURSE_TRACK_VIEW " = > " 1 "
2017-10-17 21:10:12 -04:00
) , [ " 200 " , { } ] , 0 . 1 )
2015-02-10 01:03:33 -05:00
Middleware :: RequestTracker . log_request ( data )
2015-02-05 00:08:52 -05:00
2015-07-03 17:02:57 -04:00
data = Middleware :: RequestTracker . get_data ( env (
" HTTP_USER_AGENT " = > " Mozilla/5.0 (iPhone; CPU iPhone OS 8_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B410 Safari/600.1.4 "
2017-10-17 21:10:12 -04:00
) , [ " 200 " , { " Content-Type " = > 'text/html' } ] , 0 . 1 )
2015-07-03 17:02:57 -04:00
Middleware :: RequestTracker . log_request ( data )
2020-03-24 01:28:07 -04:00
# /srv/status is never a tracked view because content-type is text/plain
data = Middleware :: RequestTracker . get_data ( env (
" HTTP_USER_AGENT " = > " kube-probe/1.18 " ,
" REQUEST_URI " = > " /srv/status?shutdown_ok=1 " ,
) , [ " 200 " , { " Content-Type " = > 'text/plain' } ] , 0 . 1 )
Middleware :: RequestTracker . log_request ( data )
2015-02-05 00:08:52 -05:00
ApplicationRequest . write_cache!
2020-03-24 01:28:07 -04:00
expect ( ApplicationRequest . http_total . first . count ) . to eq ( 4 )
expect ( ApplicationRequest . http_2xx . first . count ) . to eq ( 4 )
2015-02-05 00:08:52 -05:00
2015-07-03 17:02:57 -04:00
expect ( ApplicationRequest . page_view_anon . first . count ) . to eq ( 2 )
2015-04-25 11:18:35 -04:00
expect ( ApplicationRequest . page_view_crawler . first . count ) . to eq ( 1 )
2015-07-03 17:02:57 -04:00
expect ( ApplicationRequest . page_view_anon_mobile . first . count ) . to eq ( 1 )
2019-05-08 10:38:55 -04:00
2019-11-04 09:16:50 -05:00
expect ( ApplicationRequest . page_view_crawler . first . count ) . to eq ( 1 )
end
it " can log Discourse user agent requests correctly " do
# log discourse api agents as crawlers for page view stats...
2019-05-08 10:38:55 -04:00
data = Middleware :: RequestTracker . get_data ( env (
" HTTP_USER_AGENT " = > " DiscourseAPI Ruby Gem 0.19.0 "
) , [ " 200 " , { " Content-Type " = > 'text/html' } ] , 0 . 1 )
Middleware :: RequestTracker . log_request ( data )
ApplicationRequest . write_cache!
2019-11-04 09:16:50 -05:00
expect ( ApplicationRequest . page_view_crawler . first . count ) . to eq ( 1 )
2019-05-08 10:38:55 -04:00
2019-11-04 09:16:50 -05:00
# ...but count our mobile app user agents as regular visits
data = Middleware :: RequestTracker . get_data ( env (
" HTTP_USER_AGENT " = > " Mozilla/5.0 AppleWebKit/605.1.15 Mobile/15E148 DiscourseHub) "
) , [ " 200 " , { " Content-Type " = > 'text/html' } ] , 0 . 1 )
Middleware :: RequestTracker . log_request ( data )
ApplicationRequest . write_cache!
2017-10-17 21:10:12 -04:00
2019-11-04 09:16:50 -05:00
expect ( ApplicationRequest . page_view_crawler . first . count ) . to eq ( 1 )
expect ( ApplicationRequest . page_view_anon . first . count ) . to eq ( 1 )
end
2021-04-26 07:19:47 -04:00
context " ignore_anonymous_pageviews " do
let ( :anon_data ) do
Middleware :: RequestTracker . get_data ( env (
" HTTP_USER_AGENT " = > " Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36 "
) , [ " 200 " , { " Content-Type " = > 'text/html' } ] , 0 . 1 )
end
let ( :logged_in_data ) do
user = Fabricate ( :user , active : true )
token = UserAuthToken . generate! ( user_id : user . id )
Middleware :: RequestTracker . get_data ( env (
" HTTP_USER_AGENT " = > " Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.72 Safari/537.36 " ,
" HTTP_COOKIE " = > " _t= #{ token . unhashed_auth_token } ; "
) , [ " 200 " , { " Content-Type " = > 'text/html' } ] , 0 . 1 )
end
it " does not ignore anonymous requests for public sites " do
SiteSetting . login_required = false
Middleware :: RequestTracker . log_request ( anon_data )
Middleware :: RequestTracker . log_request ( logged_in_data )
ApplicationRequest . write_cache!
expect ( ApplicationRequest . http_total . first . count ) . to eq ( 2 )
expect ( ApplicationRequest . http_2xx . first . count ) . to eq ( 2 )
expect ( ApplicationRequest . page_view_logged_in . first . count ) . to eq ( 1 )
expect ( ApplicationRequest . page_view_anon . first . count ) . to eq ( 1 )
end
it " ignores anonymous requests for private sites " do
SiteSetting . login_required = true
Middleware :: RequestTracker . log_request ( anon_data )
Middleware :: RequestTracker . log_request ( logged_in_data )
ApplicationRequest . write_cache!
expect ( ApplicationRequest . http_total . first . count ) . to eq ( 2 )
expect ( ApplicationRequest . http_2xx . first . count ) . to eq ( 2 )
expect ( ApplicationRequest . page_view_logged_in . first . count ) . to eq ( 1 )
expect ( ApplicationRequest . page_view_anon . first ) . to eq ( nil )
end
end
2017-10-17 21:10:12 -04:00
end
2017-12-11 01:21:00 -05:00
context " rate limiting " do
class TestLogger
attr_accessor :warnings
def initialize
@warnings = 0
end
def warn ( * args )
@warnings += 1
end
end
before do
RateLimiter . enable
RateLimiter . clear_all_global!
@old_logger = Rails . logger
Rails . logger = TestLogger . new
2021-03-10 18:47:23 -05:00
# rate limiter tests depend on checks for retry-after
# they can be sensitive to clock skew during test runs
freeze_time DateTime . parse ( '2021-01-01 01:00' )
2017-12-11 01:21:00 -05:00
end
after do
Rails . logger = @old_logger
end
let :middleware do
app = lambda do | env |
[ 200 , { } , [ " OK " ] ]
end
Middleware :: RequestTracker . new ( app )
end
2019-11-18 00:05:58 -05:00
it " does nothing if configured to do nothing " do
global_setting :max_reqs_per_ip_mode , " none "
2018-01-21 21:18:30 -05:00
global_setting :max_reqs_per_ip_per_10_seconds , 1
2017-12-11 01:21:00 -05:00
status , _ = middleware . call ( env )
status , _ = middleware . call ( env )
expect ( status ) . to eq ( 200 )
end
2018-01-07 16:39:17 -05:00
it " blocks private IPs if not skipped " do
2018-01-21 21:18:30 -05:00
global_setting :max_reqs_per_ip_per_10_seconds , 1
global_setting :max_reqs_per_ip_mode , 'warn+block'
global_setting :max_reqs_rate_limit_on_private , true
2018-01-07 16:39:17 -05:00
2021-03-21 23:56:32 -04:00
addresses = %w[ 127.1.2.3 127.0.0.2 192.168.1.2 10.0.1.2 172.16.9.8 172.19.1.2 172.20.9.8 172.29.1.2 172.30.9.8 172.31.1.2 ]
warn_count = 1
addresses . each do | addr |
env1 = env ( " REMOTE_ADDR " = > addr )
2018-01-07 16:39:17 -05:00
2021-03-21 23:56:32 -04:00
status , _ = middleware . call ( env1 )
status , _ = middleware . call ( env1 )
2018-01-07 16:39:17 -05:00
2021-03-21 23:56:32 -04:00
expect ( Rails . logger . warnings ) . to eq ( warn_count )
expect ( status ) . to eq ( 429 )
warn_count += 1
end
2018-01-07 16:39:17 -05:00
end
2021-08-13 11:00:23 -04:00
it " blocks if the ip isn't static skipped " do
global_setting :max_reqs_per_ip_per_10_seconds , 1
global_setting :max_reqs_per_ip_mode , 'block'
env1 = env ( " REMOTE_ADDR " = > " 1.1.1.1 " )
status , _ = middleware . call ( env1 )
status , _ = middleware . call ( env1 )
expect ( status ) . to eq ( 429 )
end
it " doesn't block if rate limiter is enabled but IP is on the static exception list " do
stub_const ( Middleware :: RequestTracker , " STATIC_IP_SKIPPER " , " 177.33.14.73 191.209.88.192/30 " & . split & . map { | ip | IPAddr . new ( ip ) } ) do
global_setting :max_reqs_per_ip_per_10_seconds , 1
global_setting :max_reqs_per_ip_mode , 'block'
env1 = env ( " REMOTE_ADDR " = > " 177.33.14.73 " )
env2 = env ( " REMOTE_ADDR " = > " 191.209.88.194 " )
status , _ = middleware . call ( env1 )
expect ( status ) . to eq ( 200 )
status , _ = middleware . call ( env1 )
expect ( status ) . to eq ( 200 )
status , _ = middleware . call ( env2 )
expect ( status ) . to eq ( 200 )
status , _ = middleware . call ( env2 )
expect ( status ) . to eq ( 200 )
end
end
2018-01-07 16:39:17 -05:00
2018-02-05 17:45:25 -05:00
describe " register_ip_skipper " do
before do
Middleware :: RequestTracker . register_ip_skipper do | ip |
ip == " 1.1.1.2 "
end
global_setting :max_reqs_per_ip_per_10_seconds , 1
global_setting :max_reqs_per_ip_mode , 'block'
end
2018-02-05 18:38:15 -05:00
after do
Middleware :: RequestTracker . unregister_ip_skipper
end
2018-02-05 17:45:25 -05:00
it " won't block if the ip is skipped " do
env1 = env ( " REMOTE_ADDR " = > " 1.1.1.2 " )
status , _ = middleware . call ( env1 )
status , _ = middleware . call ( env1 )
expect ( status ) . to eq ( 200 )
end
it " blocks if the ip isn't skipped " do
env1 = env ( " REMOTE_ADDR " = > " 1.1.1.1 " )
status , _ = middleware . call ( env1 )
status , _ = middleware . call ( env1 )
expect ( status ) . to eq ( 429 )
end
end
2018-01-07 16:39:17 -05:00
it " does nothing for private IPs if skipped " do
2018-01-21 21:18:30 -05:00
global_setting :max_reqs_per_ip_per_10_seconds , 1
global_setting :max_reqs_per_ip_mode , 'warn+block'
global_setting :max_reqs_rate_limit_on_private , false
2018-01-07 16:39:17 -05:00
2021-03-21 23:56:32 -04:00
addresses = %w[ 127.1.2.3 127.0.3.1 192.168.1.2 10.0.1.2 172.16.9.8 172.19.1.2 172.20.9.8 172.29.1.2 172.30.9.8 172.31.1.2 ]
addresses . each do | addr |
env1 = env ( " REMOTE_ADDR " = > addr )
2018-01-07 16:39:17 -05:00
2021-03-21 23:56:32 -04:00
status , _ = middleware . call ( env1 )
status , _ = middleware . call ( env1 )
2018-01-07 16:39:17 -05:00
2021-03-21 23:56:32 -04:00
expect ( Rails . logger . warnings ) . to eq ( 0 )
expect ( status ) . to eq ( 200 )
end
2018-01-07 16:39:17 -05:00
end
it " does warn if rate limiter is enabled via warn+block " do
2018-01-21 21:18:30 -05:00
global_setting :max_reqs_per_ip_per_10_seconds , 1
global_setting :max_reqs_per_ip_mode , 'warn+block'
2018-01-07 16:39:17 -05:00
status , _ = middleware . call ( env )
2021-01-19 04:35:46 -05:00
status , headers = middleware . call ( env )
2018-01-07 16:39:17 -05:00
expect ( Rails . logger . warnings ) . to eq ( 1 )
expect ( status ) . to eq ( 429 )
2021-03-23 15:32:36 -04:00
expect ( headers [ " Retry-After " ] ) . to eq ( " 10 " )
2018-01-07 16:39:17 -05:00
end
2017-12-11 01:21:00 -05:00
it " does warn if rate limiter is enabled " do
2018-01-21 21:18:30 -05:00
global_setting :max_reqs_per_ip_per_10_seconds , 1
global_setting :max_reqs_per_ip_mode , 'warn'
2017-12-11 01:21:00 -05:00
status , _ = middleware . call ( env )
status , _ = middleware . call ( env )
expect ( Rails . logger . warnings ) . to eq ( 1 )
expect ( status ) . to eq ( 200 )
end
2018-03-05 23:20:39 -05:00
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 )
2021-01-19 04:35:46 -05:00
status , headers = middleware . call ( env1 )
2018-03-05 23:20:39 -05:00
expect ( status ) . to eq ( 429 )
2021-03-23 15:32:36 -04:00
expect ( headers [ " Retry-After " ] ) . to eq ( " 10 " )
2018-03-05 23:20:39 -05:00
env2 = env ( " REMOTE_ADDR " = > " 1.1.1.1 " )
2021-01-19 04:35:46 -05:00
status , headers = middleware . call ( env2 )
2018-03-05 23:20:39 -05:00
expect ( status ) . to eq ( 429 )
2021-03-23 15:32:36 -04:00
expect ( headers [ " Retry-After " ] ) . to eq ( " 10 " )
2018-03-05 23:20:39 -05:00
end
2017-12-11 01:21:00 -05:00
it " does block if rate limiter is enabled " do
2018-01-21 21:18:30 -05:00
global_setting :max_reqs_per_ip_per_10_seconds , 1
global_setting :max_reqs_per_ip_mode , 'block'
2017-12-11 01:21:00 -05:00
env1 = env ( " REMOTE_ADDR " = > " 1.1.1.1 " )
env2 = env ( " REMOTE_ADDR " = > " 1.1.1.2 " )
status , _ = middleware . call ( env1 )
2018-03-05 23:20:39 -05:00
expect ( status ) . to eq ( 200 )
2017-12-11 01:21:00 -05:00
2021-01-19 04:35:46 -05:00
status , headers = middleware . call ( env1 )
2017-12-11 01:21:00 -05:00
expect ( status ) . to eq ( 429 )
2021-03-23 15:32:36 -04:00
expect ( headers [ " Retry-After " ] ) . to eq ( " 10 " )
2017-12-11 01:21:00 -05:00
status , _ = middleware . call ( env2 )
expect ( status ) . to eq ( 200 )
end
end
2017-10-17 21:10:12 -04:00
context " callbacks " do
def app ( result , sql_calls : 0 , redis_calls : 0 )
lambda do | env |
sql_calls . times do
2018-03-27 02:57:19 -04:00
User . where ( id : - 100 ) . pluck ( :id )
2017-10-17 21:10:12 -04:00
end
redis_calls . times do
2019-12-03 04:05:53 -05:00
Discourse . redis . get ( " x " )
2017-10-17 21:10:12 -04:00
end
result
end
end
let :logger do
- > ( env , data ) do
@env = env
@data = data
end
end
before do
Middleware :: RequestTracker . register_detailed_request_logger ( logger )
end
after do
2018-02-05 17:45:25 -05:00
Middleware :: RequestTracker . unregister_detailed_request_logger ( logger )
2017-10-17 21:10:12 -04:00
end
2019-09-02 04:45:35 -04:00
it " can report data from anon cache " do
cache = Middleware :: AnonymousCache . new ( app ( [ 200 , { } , [ " i am a thing " ] ] ) )
tracker = Middleware :: RequestTracker . new ( cache )
uri = " /path? #{ SecureRandom . hex } "
2019-09-02 20:51:49 -04:00
request_params = {
" a " = > " b " ,
" action " = > " bob " ,
" controller " = > " jane "
}
tracker . call ( env ( " REQUEST_URI " = > uri , " ANON_CACHE_DURATION " = > 60 , " action_dispatch.request.parameters " = > request_params ) )
2019-09-04 03:18:32 -04:00
expect ( @data [ :cache ] ) . to eq ( " skip " )
2019-09-02 04:45:35 -04:00
2019-09-04 03:18:32 -04:00
tracker . call ( env ( " REQUEST_URI " = > uri , " ANON_CACHE_DURATION " = > 60 , " action_dispatch.request.parameters " = > request_params ) )
2019-09-02 04:45:35 -04:00
expect ( @data [ :cache ] ) . to eq ( " store " )
tracker . call ( env ( " REQUEST_URI " = > uri , " ANON_CACHE_DURATION " = > 60 ) )
expect ( @data [ :cache ] ) . to eq ( " true " )
2019-09-02 20:51:49 -04:00
2020-07-26 20:23:54 -04:00
# not allowlisted
2019-09-02 20:51:49 -04:00
request_params . delete ( " a " )
expect ( @env [ " action_dispatch.request.parameters " ] ) . to eq ( request_params )
2019-09-02 04:45:35 -04:00
end
2017-10-17 21:10:12 -04:00
it " can correctly log detailed data " do
2018-03-27 02:57:19 -04:00
2019-06-05 02:08:11 -04:00
global_setting :enable_performance_http_headers , true
2018-03-27 02:57:19 -04:00
# ensure pg is warmed up with the select 1 query
User . where ( id : - 100 ) . pluck ( :id )
2018-04-17 04:05:51 -04:00
freeze_time
start = Time . now . to_f
freeze_time 1 . minute . from_now
2017-10-17 21:10:12 -04:00
tracker = Middleware :: RequestTracker . new ( app ( [ 200 , { } , [ ] ] , sql_calls : 2 , redis_calls : 2 ) )
2019-06-05 02:08:11 -04:00
_ , headers , _ = tracker . call ( env ( " HTTP_X_REQUEST_START " = > " t= #{ start } " ) )
2018-04-17 04:05:51 -04:00
expect ( @data [ :queue_seconds ] ) . to eq ( 60 )
2017-10-17 21:10:12 -04:00
timing = @data [ :timing ]
expect ( timing [ :total_duration ] ) . to be > 0
expect ( timing [ :sql ] [ :duration ] ) . to be > 0
expect ( timing [ :sql ] [ :calls ] ) . to eq 2
expect ( timing [ :redis ] [ :duration ] ) . to be > 0
expect ( timing [ :redis ] [ :calls ] ) . to eq 2
2019-06-05 02:08:11 -04:00
expect ( headers [ " X-Queue-Time " ] ) . to eq ( " 60.000000 " )
expect ( headers [ " X-Redis-Calls " ] ) . to eq ( " 2 " )
expect ( headers [ " X-Redis-Time " ] . to_f ) . to be > 0
expect ( headers [ " X-Sql-Calls " ] ) . to eq ( " 2 " )
expect ( headers [ " X-Sql-Time " ] . to_f ) . to be > 0
expect ( headers [ " X-Runtime " ] . to_f ) . to be > 0
2017-10-17 21:10:12 -04:00
end
2015-02-05 00:08:52 -05:00
end
2018-03-15 17:10:45 -04:00
2015-02-05 00:08:52 -05:00
end