Skip to content

Instantly share code, notes, and snippets.

@kirkchris
Created April 13, 2016 15:20
Show Gist options
  • Save kirkchris/46c84f7fbe9b89fe461def3f4af34d4d to your computer and use it in GitHub Desktop.
Save kirkchris/46c84f7fbe9b89fe461def3f4af34d4d to your computer and use it in GitHub Desktop.
Rails 4 Middleware which extends RemoteIp to provide the correct IP for distributions which are deployed as origins to CloudFront
module ApplicationName
class Application < Rails::Application
# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
config.middleware.insert_before ActionDispatch::RemoteIp, "CloudFrontRemoteIp"
config.middleware.delete "ActionDispatch::RemoteIp"
end
# Note: This code changes in rails 5, so make sure we update before then!
# Written by Chris Kirk at Primer (https://goprimer.com)
class CloudFrontRemoteIp < ActionDispatch::RemoteIp
def call(env)
env["action_dispatch.remote_ip"] = CloudFrontGetIp.new(env, self)
@app.call(env)
end
class CloudFrontGetIp < GetIp
def calculate_ip
# Set by the Rack web server, this is a single value.
remote_addr = ips_from('REMOTE_ADDR').last
# Could be a CSV list and/or repeated headers that were concatenated.
client_ips = ips_from('HTTP_CLIENT_IP').reverse
forwarded_ips = ips_from('HTTP_X_FORWARDED_FOR').reverse
# only change needed from original code of this function to work with cloudfront
# original code here: https://github.com/rails/rails/blob/a59a9b7f729870de6c9282bd8e2a7ed7f86fc868/actionpack/lib/action_dispatch/middleware/remote_ip.rb#L109
# remove cloudfront ip
remove_cloudfront_ip(forwarded_ips)
# end of changes from original calculate_ip
# +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set.
# If they are both set, it means that this request passed through two
# proxies with incompatible IP header conventions, and there is no way
# for us to determine which header is the right one after the fact.
# Since we have no idea, we give up and explode.
should_check_ip = @check_ip && client_ips.last && forwarded_ips.last
if should_check_ip && !forwarded_ips.include?(client_ips.last)
# We don't know which came from the proxy, and which from the user
raise ActionDispatch::RemoteIp::IpSpoofAttackError, "IP spoofing attack?! " \
"HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect} " \
"HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}"
end
# We assume these things about the IP headers:
#
# - X-Forwarded-For will be a list of IPs, one per proxy, or blank
# - Client-Ip is propagated from the outermost proxy, or is blank
# - REMOTE_ADDR will be the IP that made the request to Rack
ips = [forwarded_ips, client_ips, remote_addr].flatten.compact
# If every single IP option is in the trusted list, just return REMOTE_ADDR
filter_proxies(ips).first || remote_addr
end
# supporting method has been added
def remove_cloudfront_ip(forwarded_ips)
return unless Rails.env.production? || Rails.env.staging?
forwarded_ips.shift
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment