Class: RateAttack

Inherits:
Object
  • Object
show all
Defined in:
lib/rate_attack/request.rb,
lib/rate_attack.rb,
lib/rate_attack/cache.rb,
lib/rate_attack/railtie.rb,
lib/rate_attack/version.rb,
lib/rate_attack/configuration.rb

Overview

RateAttack::Request is the same as ::Rack::Request by default.

Defined Under Namespace

Classes: Cache, Configuration, Error, Railtie, Request

Constant Summary collapse

VERSION =
"0.0.1"

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app) ⇒ RateAttack

Returns a new instance of RateAttack.



27
28
29
30
# File 'lib/rate_attack.rb', line 27

def initialize(app)
  @app = app
  @configuration = self.class.configuration
end

Class Attribute Details

.configurationObject (readonly)

Returns the value of attribute configuration.



14
15
16
# File 'lib/rate_attack.rb', line 14

def configuration
  @configuration
end

.enabledObject

Returns the value of attribute enabled.



13
14
15
# File 'lib/rate_attack.rb', line 13

def enabled
  @enabled
end

Instance Attribute Details

#configurationObject (readonly)

Returns the value of attribute configuration.



25
26
27
# File 'lib/rate_attack.rb', line 25

def configuration
  @configuration
end

Class Method Details

.cacheObject



16
17
18
# File 'lib/rate_attack.rb', line 16

def cache
  @cache ||= Cache.new
end

Instance Method Details

#call(env) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/rate_attack.rb', line 32

def call(env)
  return @app.call(env) if !self.class.enabled || env["rate_attack.called"]

  env["rate_attack.called"] = true
 
  # request = RateAttack::Request.new(env)
  s_path = sanitized_path(env['PATH_INFO'])
  if s_path =~ /rateattack/
    handle_rateattack_ratelimit_info(env)
  else
    r_path = Regexp.new(s_path)
    route = wrapped_routes.filter { sanitized_path(_1.path) =~ r_path }&.first
    route ? handle_rateattack_request(env, route) : @app.call(env)
  end
end

#get_route_ratelimit_info(env, rateattack_config) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/rate_attack.rb', line 62

def get_route_ratelimit_info(env, rateattack_config)
  count =
    Rails
      .cache
      .read(rateattack_request_key(env, rateattack_config), raw: true)
      .to_i
  {
    limit: rateattack_config[:limit],
    count: count,
    remaining: (rateattack_config[:limit] - count),
    action: (rateattack_config[:action] || sanitized_path(env['PATH_INFO'])),
  }
end

#guess_ip(request) ⇒ Object



140
141
142
143
144
145
146
147
148
# File 'lib/rate_attack.rb', line 140

def guess_ip(request)
  ip = request.remote_ip || request.ip
  ip = '127.0.0.1' if ip == '::1'
  begin
    IPAddress.valid?(ip)
  rescue StandardError
    nil
  end
end

#handle_rateattack_ratelimit_info(env) ⇒ Object



80
81
82
83
84
85
86
87
# File 'lib/rate_attack.rb', line 80

def handle_rateattack_ratelimit_info(env)
  ra_ratelimit_infos =
    wrapped_routes.map do
      get_route_ratelimit_info(env, _1.defaults[:rateattack])
    end

  [200, {}, StringIO.new({ data: ra_ratelimit_infos }.to_json)]
end

#handle_rateattack_request(env, route) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/rate_attack.rb', line 89

def handle_rateattack_request(env, route)
  limit_key = rateattack_request_key(env, route.defaults[:rateattack])
  current_info = get_route_ratelimit_info(env, route.defaults[:rateattack])

  if (current_info[:limit] - current_info[:count]) <= 0
    return [
      429,
      set_rate_limit_and_merge_headers(current_info),
      StringIO.new(I18n.t('general.ratelimit.exceeded'))
    ]
  end

  @status, @headers, @response = @app.call(env)

  if @status >= 200 && @status <= 400
    current_info[:count] =
      Rails.cache.increment(
        limit_key,
        expires_in: route.defaults[:rateattack][:duration],
      )
  end

  [
    @status,
    set_rate_limit_and_merge_headers(current_info, headers: @headers),
    @response,
  ]
end

#rateattack_request_key(env, info) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/rate_attack.rb', line 48

def rateattack_request_key(env, info)
  request = ActionDispatch::Request.new env
  keys = []
  if info[:uniqueness].is_a?(Array)
    keys =
      info[:uniqueness].map do |item|
        item.to_s == 'ip' ? guess_ip(request) : env[item.to_s]
      end
  end
  keys.insert(0, 'RateAttack')
  keys.push(info[:action] || sanitized_path(env['PATH_INFO']))
  keys.join('::')
end

#remove_prefix_postfix_slash(str) ⇒ Object



123
124
125
# File 'lib/rate_attack.rb', line 123

def remove_prefix_postfix_slash(str)
  str.gsub(%r{^\/}, '').gsub(%r{\/$}, '')
end

#sanitized_path(path) ⇒ Object



76
77
78
# File 'lib/rate_attack.rb', line 76

def sanitized_path(path)
  path.gsub(%r{^\/}, '').gsub(/\(.:format\)/, '')
end

#set_rate_limit_and_merge_headers(rateattack_info, headers: {}) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/rate_attack.rb', line 127

def set_rate_limit_and_merge_headers(rateattack_info, headers: {})
  headers.merge(
    {
      I18n.t('general.ratelimit.headers.remaining') => [
        rateattack_info[:limit] - rateattack_info[:count],
        0,
      ].max.to_s,
      I18n.t('general.ratelimit.headers.limit') =>
        rateattack_info[:limit].to_s,
    },
  )
end

#wrapped_routesObject



118
119
120
121
# File 'lib/rate_attack.rb', line 118

def wrapped_routes
  Rails.application.routes.routes.filter { _1.defaults[:rateattack] }.compact
    .map { ActionDispatch::Routing::RouteWrapper.new(_1) }
end