diff --git a/Gemfile b/Gemfile index 517bee1e..09931657 100644 --- a/Gemfile +++ b/Gemfile @@ -24,6 +24,9 @@ gem 'redis' gem 'redis-rails' +# Provides a low-level time-based throttle. +gem 'prorate' + # Use RabbitMQ gem 'bunny' gem 'sneakers' diff --git a/Gemfile.lock b/Gemfile.lock index 0bf99257..a699a52c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -611,6 +611,8 @@ GEM parser (3.1.2.0) ast (~> 2.4.1) promise.rb (0.7.4) + prorate (0.7.3) + redis (>= 2) pry (0.14.1) coderay (~> 1.1) method_source (~> 1.0) @@ -685,7 +687,7 @@ GEM activesupport (>= 6.1.5) i18n rbtree (0.4.5) - redis (4.6.0) + redis (4.8.1) redis-actionpack (5.3.0) actionpack (>= 5, < 8) redis-rack (>= 2.1.0, < 3) @@ -937,6 +939,7 @@ DEPENDENCIES opentelemetry-sdk overcommit pagy + prorate pry puma pundit (~> 2.3) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index fd6e8e47..18e4f042 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -8,13 +8,40 @@ def execute variables = prepare_variables(params[:variables]) query = params[:query] operation_name = params[:operationName] + context = { current_user: current_user, sign_out: method(:sign_out), cookies: cookies } + + t = Prorate::Throttle.new( + name: "global-api-limit", + limit: 1000, + period: 1.hour, + block_for: 1.hour, + redis: throttle_redis, + logger: Rails.logger + ) + + real_ip = + case request.host + when 'compass.gitee.com' + request.env['HTTP_X_FORWARDED_FOR']&.split(',')&.first + else + request.remote_ip + end + + t << real_ip + + t.throttle! if !current_user || real_ip + result = CompassWebServiceSchema.execute(query, variables: variables, context: context, operation_name: operation_name) render json: result + rescue Prorate::Throttled => e + logger.warn("blocking #{request.remote_ip} Retry-After #{e.retry_in_seconds}") + response.set_header('Retry-After', e.retry_in_seconds.to_s) + render json: { errors: [{ message: e.message, retry_fater: e.retry_in_seconds }], data: {} }, status: 429 rescue StandardError => e raise e unless Rails.env.development? handle_error_in_development(e) @@ -22,6 +49,10 @@ def execute private + def throttle_redis + @throttle_redis ||= Redis.new(url: ENV.fetch('REDIS_URL') { 'redis://redis:6379/1' }) + end + # Handle variables in form data, JSON body, or a blank value def prepare_variables(variables_param) case variables_param