diff --git a/Gemfile b/Gemfile index 751c9ceb7..465e25942 100644 --- a/Gemfile +++ b/Gemfile @@ -26,6 +26,7 @@ gem 'active_model_serializers', '~> 0.10.7' gem 'json-jwt' gem 'jwt', '~> 1.5' gem 'kaminari' +gem 'rack-attack' # JS datetime library, requirement of datetime picker gem 'momentjs-rails', '~> 2.17.1' diff --git a/Gemfile.lock b/Gemfile.lock index 661bcdd0f..8bc33831c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -357,6 +357,8 @@ GEM public_suffix (3.0.2) puma (3.11.2) rack (2.0.5) + rack-attack (5.4.1) + rack (>= 1.0, < 3) rack-test (1.1.0) rack (>= 1.0, < 3) rails (5.1.6) @@ -595,6 +597,7 @@ DEPENDENCIES pry-byebug pry-rails puma + rack-attack rails (= 5.1.6) rails-controller-testing rails_12factor diff --git a/Makefile b/Makefile index a57f403b6..aac0cce07 100644 --- a/Makefile +++ b/Makefile @@ -85,7 +85,7 @@ tests-ci: @docker-compose run --rm web bash -c "bundle install && npm install" @docker-compose up -d webpack @docker-compose ps - @docker-compose run -e ENABLE_EMAIL_CONFIRMATIONS=false -e MAILER_PORT=$MAILER_PORT -e SMTP_DOMAIN=$SMTP_DOMAIN -e SMTP_USERNAME=$SMTP_USERNAME -e SMTP_PASSWORD=$SMTP_PASSWORD -e SMTP_ADDRESS=$SMTP_ADDRESS -e PAPERCLIP_HASH_SECRET=PAPERCLIP_HASH_SECRET -e MAIL_SERVER_URL=localhost -e PAPERCLIP_STORAGE=filesystem -e ENABLE_RECAPTCHA=false -e ENABLE_USER_CONFIRMATION=false -e ENABLE_USER_REGISTRATION=true --rm web bash -c "rake db:create db:migrate && rake db:migrate RAILS_ENV=test && npm install && bundle exec rspec && bundle exec cucumber" + @docker-compose run -e ENABLE_EMAIL_CONFIRMATIONS=false -e MAILER_PORT=$MAILER_PORT -e SMTP_DOMAIN=$SMTP_DOMAIN -e SMTP_USERNAME=$SMTP_USERNAME -e SMTP_PASSWORD=$SMTP_PASSWORD -e SMTP_ADDRESS=$SMTP_ADDRESS -e PAPERCLIP_HASH_SECRET=PAPERCLIP_HASH_SECRET -e MAIL_SERVER_URL=localhost -e PAPERCLIP_STORAGE=filesystem -e ENABLE_RECAPTCHA=false -e ENABLE_USER_CONFIRMATION=false -e ENABLE_USER_REGISTRATION=true -e CORE_API_RATE_LIMIT=1000000 --rm web bash -c "rake db:create db:migrate && rake db:migrate RAILS_ENV=test && npm install && bundle exec rspec && bundle exec cucumber" console: @$(MAKE) rails cmd="rails console" diff --git a/app/services/api.rb b/app/services/api.rb index a06f328be..3dd4a5f52 100644 --- a/app/services/api.rb +++ b/app/services/api.rb @@ -17,6 +17,7 @@ module Api attr_accessor :core_api_token_iss attr_accessor :azure_ad_apps attr_accessor :core_api_v1_preview + attr_accessor :core_api_rate_limit def initialize @core_api_sign_alg = 'HS256' @@ -24,6 +25,7 @@ module Api @core_api_token_iss = 'SciNote' @azure_ad_apps = {} @core_api_v1_preview = false + @core_api_rate_limit = 1000 end end end diff --git a/config/application.rb b/config/application.rb index d76340c7b..d23a11c93 100644 --- a/config/application.rb +++ b/config/application.rb @@ -15,6 +15,9 @@ module Scinote # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. + # Add rack-attack middleware for request rate limiting + config.middleware.use Rack::Attack + # Swap the Rack::MethodOverride with a wrapped middleware for WOPI handling require_relative '../app/middlewares/wopi_method_override' config.middleware.swap Rack::MethodOverride, WopiMethodOverride diff --git a/config/initializers/api.rb b/config/initializers/api.rb index 2d51d419f..71db82830 100644 --- a/config/initializers/api.rb +++ b/config/initializers/api.rb @@ -9,6 +9,8 @@ Api.configure do |config| config.core_api_token_iss = ENV['CORE_API_TOKEN_ISS'] end + config.core_api_rate_limit = ENV['CORE_API_RATE_LIMIT'].to_i || 1000 + config.core_api_v1_preview = true if ENV['CORE_API_V1_PREVIEW'] Paperclip::DataUriAdapter.register if ENV['CORE_API_V1_PREVIEW'] diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb new file mode 100644 index 000000000..e216a8b52 --- /dev/null +++ b/config/initializers/rack_attack.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +Rack::Attack.throttle('api requests by ip', + limit: Api.configuration.core_api_rate_limit, + period: 60) do |request| + request.ip if request.path =~ %r{^\/api\/} +end + +Rack::Attack.throttled_response = lambda do |env| + match_data = env['rack.attack.match_data'] + now = match_data[:epoch_time] + + headers = { + 'RateLimit-Limit' => match_data[:limit].to_s, + 'RateLimit-Remaining' => '0', + 'RateLimit-Reset' => ( + now + (match_data[:period] - now % match_data[:period]) + ).to_s + } + + [429, headers, ["Throttled\n"]] +end