How to make Rack::Attack work behind a load balancer? - ruby-on-rails

I used the example throttle code for Rack::Attack.
throttle('req/ip', limit: 100, period: 5.minutes) do |req|
req.ip unless req.path.starts_with?('/assets')
end
This worked great on our staging server but immediately ran into the limit on production because req.ip returns the IP address of our load balancer and not the remote_ip of the client.
Note that remote_ip is a method in ActionDispatch::Request but not Rack::Attack::Request.
We are using Rails 3.2.2 on Ruby 2.2.

I was able to get it working by adding a method to Rack::Attack::Request
class Rack::Attack
class Request < ::Rack::Request
def remote_ip
#remote_ip ||= (env['action_dispatch.remote_ip'] || ip).to_s
end
end
end
then using
req.remote_ip unless req.path.starts_with?('/assets')

Related

ActiveStorage: Old urls still request deprecated :combine_options in variation_key

Recently I upgraded from Rails 6.0.3.4 to 6.1.3. ActiveStorage deprecated combine_options, which I cleared from my app. All fresh request work as expected.
Internet Bots (Facebook, Google, ...) cache urls to images hosted on a website (like mine). According to my Rollbar records they request these a couple of times a day.
The cached URL's that should load ActiveStorage attachments include an old variation_key in the URL. When the blob wants to load using the decoded variation_key, I see that combine_options is still present. This throws a 500 Internal Server Error with ArgumentError (Active Storage's ImageProcessing transformer doesn't support :combine_options, as it always generates a single ImageMagick command.):.
Is there any way I can stop these errors from showing up?
Rails version: 6.1.3.
Ruby version: 2.7.2p137
I have resolved this issue using some middleware. This will intercept all incoming requests, scan if they are ActiveStorage urls, find the ones with the deprecated combine_options and just return 404 not found. This code will also raise an error is the current environment is development, this way I don't accidentally introduce the deprecated code again.
For those of you who might have the same problem, here's the code.
application.rb
require_relative '../lib/stopper'
config.middleware.use ::Stopper
lib/stopper.rb
class Stopper
def initialize(app)
#app = app
end
def call(env)
req = Rack::Request.new(env)
path = req.path
if problematic_active_storage_url?(path)
if ENV["RACK_ENV"] == 'development'
raise "Problematic route, includes deprecated combine_options"
end
[404, {}, ['not found']]
else
#app.call(env)
end
end
def problematic_active_storage_url?(path)
if active_storage_path?(path) && !valid_variation_key?(variation_key_from_path(path))
return true
end
false
end
def active_storage_path?(path)
path.start_with?("/rails/active_storage/representations/")
end
def variation_key_from_path(path)
if path.start_with?("/rails/active_storage/representations/redirect/")
path.split('/')[6]
elsif path.start_with?("/rails/active_storage/representations/")
path.split('/')[5]
end
end
def valid_variation_key?(var_key)
if decoded_variation = ActiveStorage::Variation.decode(var_key)
if transformations = decoded_variation.transformations
if transformations[:combine_options].present?
return false
end
end
end
true
end
end
I thought the stopper was a great solution but eventually I wanted to get rid of it. Unforunately most of our old requests were stilling coming through months later and no one was honoring the 404s. So I decided to monkey patch based off the previous rails versions. This is was I did.
config/initalizers/active_storage.rb
Rails.application.config.after_initialize do
require 'active_storage'
ActiveStorage::Transformers::ImageProcessingTransformer.class_eval do
private
def operations
transformations.each_with_object([]) do |(name, argument), list|
if name.to_s == "combine_options"
list.concat argument.keep_if { |key, value| value.present? and key.to_s != "host" }.to_a
elsif argument.present?
list << [ name, argument ]
end
end
end
end
end

Rack Attack Throttling

I am trying to throttle some bots trying to brute force authentication on our production server.
This is a Rails 4 app with rack attack 6.3 and i have configured it like this:
config/initializers/rack_attack.rb
class Rack::Attack
# Throttle all requests by IP (60rpm)
#
# Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}"
throttle('req/ip', limit: 300, period: 5.minutes) do |req|
unless req.path.start_with?('/assets')
Rails.logger.error("Rack::Attack Too many requests from IP: #{req.ip}")
req.ip
end
end
### Prevent Brute-Force Attacks ###
# Throttle any POST requests by IP address
#
# Key: "rack::attack:#{Time.now.to_i/:period}:pink/posts/ip:#{req.ip}"
throttle('pink/posts/ip', limit: 1, period: 2.seconds) do |req|
if req.post?
Rails.logger.error("Rack::Attack Too many POSTS from IP: #{req.ip}")
req.ip
end
end
end
and yet i keep getting millions of requests from the same IP, am i missing something?
The docs say that rails apps use it by default so this should be the only configuration necessary to enable throttling.
So in the end both syntax like what I had and what #wscourge suggested work, the problem is that even though the official docs say that rails apps use it by default, you still need to add the following to application.rb, at least in Rails 4:
config.middleware.use Rack::Attack
From what I see in the throttling documentation syntax, the right way to do it is to execute the class method in the initializer, and not to execute it in the class definition:
config/initializers/rack_attack.rb
# Throttle all requests by IP (60rpm)
#
# Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}"
Rack::Attack.throttle('req/ip', limit: 300, period: 5.minutes) do |req|
unless req.path.start_with?('/assets')
Rails.logger.error("Rack::Attack Too many requests from IP: #{req.ip}")
req.ip
end
end
### Prevent Brute-Force Attacks ###
# Throttle any POST requests by IP address
#
# Key: "rack::attack:#{Time.now.to_i/:period}:pink/posts/ip:#{req.ip}"
Rack::Attack.throttle('pink/posts/ip', limit: 1, period: 2.seconds) do |req|
if req.post?
Rails.logger.error("Rack::Attack Too many POSTS from IP: #{req.ip}")
req.ip
end
end

Uninitialized constant error after uploading on Heroku

There is the following problem: I'm developing some Rails application on my local machine, and all is good, app works, but after uploading on Heroku there would be the following error (I saw it using 'heroku logs'):
NameError (uninitialized constant Api::V1::ApiV1Controller::UndefinedTokenTypeError)
My code:
def require_token
begin
Some code which generates UndefinedTokenTypeError
rescue UndefinedTokenTypeError => e
render json: e.to_json
end
end
UndefinedTokenTypeError is in lib/errors.rb file:
class EmptyCookieParamsError < StandardError
def to_json
{ result_code: 1 }
end
end
class UndefinedTokenTypeError < StandardError
def to_json
{ result_code: 2 }
end
end
I've got the same version for Rails/Ruby on my local machine (2.0). How can I fix it? Thanks.
From what I can see, you may be experiencing either a CORS-related issue or you're not authenticating properly
Cross Origin Resource Sharing
CORS is a standard HTML protocol, which basically governs which websites can "ping" your site. Facebook & Twitter's third-party widgets only work because they allow any site to send them data
For Rails to work with CORS, it's recommended to install the Rack-CORS gem. This will allow you to put this code in your config/application.rb file:
#CORS
config.middleware.use Rack::Cors do
allow do
origins '*'
resource '/data*', :headers => :any, :methods => :post
end
end
Because you're experiencing these issues on Heroku, it could be the problem you're experiencing. Even if it isn't, it's definitely useful to appreciate how CORS works
Authentication
Unless your API is public, you'll likely be authenticating the requests
The way we do this is with the authenticate_or_request_with_http_token function, which can be seen here:
#Check Token
def restrict_access
authenticate_or_request_with_http_token do |token, options|
user = User.exists?(public_key: token)
#token = token if user
end
end
We learnt how to do this with this Railscast, which discusses how to protect an API. The reason I asked about your code was because the above works for us on Heroku, and you could gain something from it!
Running on Heroku will be using the production environment. Check to see what is different between environments/development.rb and environments/production.rb
You can try running your app in production mode on your local machine, rails server -e production
I am guessing your config.autoload_paths isn't set correctly. Should be in config/application.rb

Adding an SSL Cert on heroku with godaddy

So here is the issue.
I have a site with SSL. (https://www.archerandreed.com/) It works great.
When you type archerandreed.com/ in the browser everything still works great.
Unfortunately when you type https://archerandreed.com/ or http://archerandreed.com/ into your browser you are given an SSL cert warning.
I thought I could just add a cert for www.archerandreed.com && archerandreed.com but heroku no longer accepts 2 ssl endpoints.
So what are the possible solutions. I assume one solution is to buy a wild card domain but that is painful. My app is rails 3.2.6. Is it possible to only force SSL if their is a subdomain? Can I do that in routes.rb or in config/environments/production.rb? Thanks for any help in advance.
So I think I found what I was looking for and I think this should be documented somehow in heroku...
1) turn off config.force_ssl = true:
config.force_ssl = false # config/environments/production.rb
2) in application_controller have the following:
class ApplicationController < ActionController::Base
before_filter :ensure_domain
before_filter :force_ssl
APP_DOMAIN = 'www.archerandreed.com'
protected
def force_ssl
if Rails.env.production?
redirect_to :protocol => 'https' unless request.ssl?
end
end
def ensure_domain
if Rails.env.production? && ((request.env['HTTP_HOST'] != APP_DOMAIN) )
# HTTP 301 is a "permanent" redirect
redirect_to( "https://#{APP_DOMAIN}", :status => 301) and return
end
end
end
Another solution (as brought up in a couple other threads - e.g. here) is to use a DNS provider like DNSimple that allows the use of ALIAS records. Then add an ALIAS record for your naked domain (in this case archerandreed.com), since you don't want to use A records for pointing your naked domain at heroku anyway.
You can then use config.force_ssl in production.rb and not have to add filters to your application controller.

Rails - Ensuring www is in the URL

I have my app hosted on Heroku, and have a cert for www.mysite.com
I'm trying to solve for
Ensuring www is in the URL, and that the URL is HTTPS
Here's what I have so far:
class ApplicationController < ActionController::Base
before_filter :check_uri
def check_uri
redirect_to request.protocol + "www." + request.host_with_port + request.request_uri if !/^www/.match(request.host) if Rails.env == 'production'
end
But this doesn't seem to being working. Any suggestions or maybe different approaches to solve for ensuring HTTPs and www. is in the URL?
Thanks
For the SSL, use rack-ssl.
# config/environments/production.rb
MyApp::Application.configure do
require 'rack/ssl'
config.middleware.use Rack::SSL
# the rest of the production config....
end
For the WWW, create a Rack middleware of your own.
# lib/rack/www.rb
class Rack::Www
def initialize(app)
#app = app
end
def call(env)
if env['SERVER_NAME'] =~ /^www\./
#app.call(env)
else
[ 307, { 'Location' => 'https://www.my-domain-name.com/' }, '' ]
end
end
end
# config/environments/production.rb
MyApp::Application.configure do
config.middleware.use Rack::Www
# the rest of the production config....
end
To test this in the browser, you can edit your /etc/hosts file on your local development computer
# /etc/hosts
# ...
127.0.0.1 my-domain-name.com
127.0.0.1 www.my-domain-name.com
run the application in production mode on your local development computer
$ RAILS_ENV=production rails s -p 80
and browse to http://my-domain-name.com/ and see what happens.
For the duration of the test, you may want to comment out the line redirecting you to the HTTPS site.
There may also be ways to test this with the standard unit-testing and integration-testing tools that many Rails projects use, such as Test::Unit and RSpec.
Pivotal Labs has some middleware called Refraction that is a mod_rewrite replacement, except it lives in your source code instead of your Apache config.
It may be a little overkill for what you need, but it handles this stuff pretty easily.
In Rails 3
#config/routes.rb
Example::Application.routes.draw do
redirect_proc = Proc.new { redirect { |params, request|
URI.parse(request.url).tap { |x| x.host = "www.example.net"; x.scheme = "https" }.to_s
} }
constraints(:host => "example.net") do
match "(*x)" => redirect_proc.call
end
constraints(:scheme => "http") do
match "(*x)" => redirect_proc.call
end
# ....
# .. more routes ..
# ....
end
I think the issue is you are running on Heroku. Check the Heroku documentation regarding Wildcard domains:
"If you'd like your app to respond to any subdomain under your custom domain name (as in *.yourdomain.com), you’ll need to use the wildcard domains add-on. ..."
$ heroku addons:add wildcard_domains
Also look at Redirecting Traffic to Specific Domain:
"If you have multiple domains, or your app has users that access it via its Heroku subdomain but you later switched to your own custom domain, you will probably want to get all users onto the same domain with a redirect in a before filter. Something like this will do the job:"
class ApplicationController
before_filter :ensure_domain
TheDomain = 'myapp.mydomain.com'
def ensure_domain
if request.env['HTTP_HOST'] != TheDomain
redirect_to TheDomain
end
end
end
Try this
def check_uri
if Rails.env == 'production' && request && (request.subdomains.first != "www" || request.protocol != 'https://')
redirect_to "https://www.mysite.com" + request.path, :status => 301 and return
end
end
Your best bet would be to set up redirect with your DNS provider, so it happens long before any request reaches your server. From the Heroku Dev Center:
Subdomain redirection results in a 301 permanent redirect to the specified subdomain for all requests to the naked domain so all current and future requests are properly routed and the full www hostname is displayed in the user’s location field.
DNSimple provides a convenient URL redirect seen here redirecting from
the heroku-sslendpoint.com naked domain to the
www.heroku-sslendpoint.com subdomain.
For proper configuration on Heroku the www subdomain should then be a
CNAME record reference to yourappname.herokuapp.com.
It's not just DNSimple that does this. My DNS provider is 123 Reg and they support it but call it web forwarding.

Resources