├── .rspec ├── lib ├── tasks │ └── rspec.rake ├── remote_ip_proxy_scrubber │ ├── version.rb │ ├── filter_proxy_ips.rb │ └── remote_ip_logger.rb └── remote_ip_proxy_scrubber.rb ├── Gemfile ├── .travis.yml ├── Rakefile ├── .gitignore ├── LICENSE ├── remote_ip_proxy_scrubber.gemspec ├── README.md └── spec ├── remote_ip_proxy_scrubber_spec.rb ├── spec_helper.rb └── filter_proxy_ips_spec.rb /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /lib/tasks/rspec.rake: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | RSpec::Core::RakeTask.new(:specs) 4 | 5 | task :default => :specs -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | if RUBY_VERSION > "1.9" 6 | gem "codeclimate-test-reporter", :group => :test, :require => nil 7 | end 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.0 4 | - 2.1.0 5 | - 1.9.3 6 | - 1.8.7 7 | addons: 8 | code_climate: 9 | repo_token: b4467c40c37d79974e72fddf47335d9cd65596e3b2cf64aeac7594a6ba12f85e 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require "bundler/gem_tasks" 3 | rescue LoadError 4 | puts "Bundler not available. Install it with: gem install bundler" 5 | end 6 | 7 | Dir[File.join(File.dirname(__FILE__), "lib/tasks/*.rake")].sort.each { |ext| load ext } 8 | -------------------------------------------------------------------------------- /lib/remote_ip_proxy_scrubber/version.rb: -------------------------------------------------------------------------------- 1 | module RemoteIpProxyScrubber 2 | class Version 3 | MAJOR = 0 4 | MINOR = 1 5 | PATCH = 1 6 | STRING = "#{MAJOR}.#{MINOR}.#{PATCH}" 7 | 8 | class << self 9 | # A String representing the current version of the OEmbed gem. 10 | def inspect 11 | STRING 12 | end 13 | alias_method :to_s, :inspect 14 | end 15 | end 16 | 17 | VERSION = Version::STRING 18 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | 12 | ## Specific to RubyMotion: 13 | .dat* 14 | .repl_history 15 | build/ 16 | 17 | ## Documentation cache and generated files: 18 | /.yardoc/ 19 | /_yardoc/ 20 | /doc/ 21 | /rdoc/ 22 | 23 | ## Environment normalisation: 24 | /.bundle/ 25 | /lib/bundler/man/ 26 | 27 | # for a library or gem, you might want to ignore these files since the code is 28 | # intended to run in multiple environments; otherwise, check them in: 29 | # Gemfile.lock 30 | # .ruby-version 31 | # .ruby-gemset 32 | 33 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 34 | .rvmrc 35 | .ruby-version 36 | .ruby-gemset 37 | 38 | # This is a gem, so don't commit the Gemfile.lock 39 | /Gemfile.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Marcos Wright-Kuhns 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /remote_ip_proxy_scrubber.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | lib = File.expand_path('../lib/', __FILE__) 4 | $:.unshift lib unless $:.include?(lib) 5 | 6 | require 'remote_ip_proxy_scrubber/version' 7 | 8 | Gem::Specification.new do |s| 9 | s.name = "remote_ip_proxy_scrubber" 10 | s.version = RemoteIpProxyScrubber::Version.to_s 11 | 12 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 13 | s.authors = ["Marcos Wright-Kuhns"] 14 | s.date = "2015-03-20" 15 | s.description = "Make it as easy as possible to prevent Rails from logging IP addresses that belong to proxy devices in your HTTP request chain." 16 | s.email = "marcos@wrightkuhns.com" 17 | s.homepage = "http://github.com/metavida/remote_ip_proxy_scrubber" 18 | s.licenses = ["MIT"] 19 | 20 | s.files = `git ls-files`.split("\n") 21 | s.test_files = s.files.grep(%r{^(test|spec|features,integration_test)/}) 22 | 23 | s.require_paths = ["lib"] 24 | s.rubygems_version = "2.4.6" 25 | s.summary = "Help Rails ignore IPs for proxy devices" 26 | 27 | if s.respond_to? :specification_version then 28 | s.specification_version = 3 29 | 30 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 31 | s.add_development_dependency(%q, [">= 0"]) 32 | s.add_development_dependency(%q, ["~> 3.0"]) 33 | else 34 | s.add_dependency(%q, [">= 0"]) 35 | s.add_dependency(%q, ["~> 3.0"]) 36 | end 37 | else 38 | s.add_dependency(%q, [">= 0"]) 39 | s.add_dependency(%q, ["~> 3.0"]) 40 | end 41 | end 42 | 43 | -------------------------------------------------------------------------------- /lib/remote_ip_proxy_scrubber/filter_proxy_ips.rb: -------------------------------------------------------------------------------- 1 | require 'ipaddr' 2 | 3 | module RemoteIpProxyScrubber 4 | module Rails4 5 | class FilterProxyIPs 6 | X_FORWARDED_FOR = 'HTTP_X_FORWARDED_FOR' 7 | 8 | attr_reader :proxies 9 | 10 | def initialize(app, *proxy_matchers) 11 | @app = app 12 | 13 | @proxies = [] 14 | 15 | proxy_matchers.flatten.each do |matcher| 16 | @proxies << case matcher 17 | when Regexp, IPAddr 18 | matcher 19 | when String 20 | IPAddr.new(matcher) 21 | else 22 | raise ArgumentError.new("Expected String, IPAddr or Regexp but found #{matcher.class} #{matcher.inspect}") 23 | end 24 | end 25 | 26 | end 27 | 28 | def call(env) 29 | @env = env 30 | 31 | ips = ips_from(X_FORWARDED_FOR) 32 | @env[X_FORWARDED_FOR] = filter_proxies(ips).join(', ') 33 | 34 | @app.call(@env) 35 | end 36 | 37 | protected 38 | 39 | # Shamelssly copied from Rails 4.2 40 | # https://github.com/rails/rails/blob/v4.2.0/actionpack/lib/action_dispatch/middleware/remote_ip.rb#L149-L168 41 | def ips_from(header) 42 | # Split the comma-separated list into an array of strings 43 | ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : [] 44 | ips.select do |ip| 45 | begin 46 | # Only return IPs that are valid according to the IPAddr#new method 47 | range = IPAddr.new(ip).to_range 48 | # we want to make sure nobody is sneaking a netmask in 49 | range.begin == range.end 50 | rescue ArgumentError 51 | nil 52 | end 53 | end 54 | end 55 | 56 | def filter_proxies(ips) 57 | ips.reject do |ip| 58 | @proxies.any? { |proxy| proxy === ip } 59 | end 60 | end 61 | end 62 | end 63 | end -------------------------------------------------------------------------------- /lib/remote_ip_proxy_scrubber.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.dirname(__FILE__) 2 | 3 | require 'remote_ip_proxy_scrubber/version' 4 | 5 | module RemoteIpProxyScrubber 6 | # Add the following to config/application.rb or conifg/environments/* 7 | # 8 | # config.middleware.insert_before(Rails::Rack::Logger, RemoteIpProxyScrubber.filter_middleware, [ 9 | # "216.73.93.70/31", # www.google.com 10 | # "216.73.93.72/31", # www.google.com 11 | # "17.0.0.0/8", # Apple 12 | # ]) 13 | def filter_middleware 14 | require 'remote_ip_proxy_scrubber/filter_proxy_ips' 15 | 16 | RemoteIpProxyScrubber::Rails4::FilterProxyIPs 17 | end 18 | module_function :filter_middleware 19 | 20 | # Returns a Class to be used a rack middleware, 21 | # replacing the existing `Rails::Rack::Logger`. 22 | # 23 | # config.middleware.insert_before(Rails::Rack::Logger, RemoteIpProxyScrubber.patched_logger) 24 | # config.middleware.delete(Rails::Rack::Logger) 25 | def patched_logger 26 | require 'remote_ip_proxy_scrubber/remote_ip_logger' 27 | 28 | rails_version = self.rails_version 29 | if rails_version >= Gem::Version.new('4.0.0') 30 | RemoteIpProxyScrubber::Rails4::RemoteIPLogger 31 | elsif rails_version >= Gem::Version.new('3.2.9') 32 | RemoteIpProxyScrubber::Rails3_2_9::RemoteIPLogger 33 | elsif rails_version >= Gem::Version.new('3.2.0') 34 | RemoteIpProxyScrubber::Rails3_2_0::RemoteIPLogger 35 | elsif rails_version >= Gem::Version.new('3.0.6') 36 | RemoteIpProxyScrubber::Rails3_1::RemoteIPLogger 37 | elsif rails_version >= Gem::Version.new('3.0.0') 38 | RemoteIpProxyScrubber::Rails3_0::RemoteIPLogger 39 | else 40 | fail "Sorry, this gem doesn't know how to monkey-patch the Rails logger for Rails #{rails_version} yet." 41 | end 42 | end 43 | module_function :patched_logger 44 | 45 | def rails_version 46 | rails_version = Rails.version rescue nil 47 | rails_version ||= RAILS_GEM_VERSION rescue nil 48 | if rails_version.nil? 49 | fail "Unable to determine the current version of Rails" 50 | end 51 | Gem::Version.new(rails_version) 52 | end 53 | module_function :rails_version 54 | 55 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/remote_ip_proxy_scrubber.svg)](http://badge.fury.io/rb/remote_ip_proxy_scrubber) 2 | [![Build Status](https://travis-ci.org/metavida/remote_ip_proxy_scrubber.svg?branch=master)](https://travis-ci.org/metavida/remote_ip_proxy_scrubber) 3 | [![Code Climate](https://codeclimate.com/github/metavida/remote_ip_proxy_scrubber/badges/gpa.svg)](https://codeclimate.com/github/metavida/remote_ip_proxy_scrubber) 4 | [![Test Coverage](https://codeclimate.com/github/metavida/remote_ip_proxy_scrubber/badges/coverage.svg)](https://codeclimate.com/github/metavida/remote_ip_proxy_scrubber) 5 | 6 | # remote_ip Proxy Scrubber 7 | 8 | A project that makes it as easy as possible to prevent Rails from logging IP addresses that belong to proxy devices in your HTTP request chain. 9 | 10 | Because Rails has pretty dramatically changed how these sorts of IPs are filtered over the years, this project's ultimate goal is to make life easy on as many Rails versions as possible. 11 | 12 | # Common Use-cases 13 | 14 | * Using [CloudFlare in front of your app](https://www.cloudflare.com/ips) 15 | * Using [Incapsula in front of your app](https://incapsula.zendesk.com/hc/en-us/articles/200627570-Restricting-direct-access-to-your-website-Incapsula-s-IP-addresses-) 16 | 17 | # Usage 18 | 19 | Let's say you've got proxy servers running outside of the local network where your Rails/Rack app is running. In this example, we'll say the IP addresses of these proxy servers are in these IP ranges: `17.0.0.4/30`, `17.17.0.8/30` 20 | 21 | ## Install the gem 22 | 23 | Add the following line to your application's Gemfile: 24 | 25 | ```ruby 26 | gem 'remote_ip_proxy_scrubber' 27 | ``` 28 | 29 | Then run `bundle install` to install the gem. 30 | 31 | ## Rails: Fixing `request.remote_ip` 32 | 33 | Without this gem, calls to `request.remote_ip` from your Rails app will return the IP addresses from your proxy servers. Adding the code, below, ensures that `request.remote_ip` will never return the IP addresses of your proxy servers, and assuming the servers that first process requests from your clients is adding an appropriate X-Forwarded-For header, `request.remote_ip` will return the real IP address of your clients! 34 | 35 | ```ruby 36 | # Add the following to config/application.rb or conifg/environments/*.rb 37 | 38 | config.middleware.insert_before(Rails::Rack::Logger, RemoteIpProxyScrubber.filter_middleware, [ 39 | "17.0.0.4/30", 40 | "17.17.0.8/30", 41 | ]) 42 | ``` 43 | 44 | ## Rails: Fixing logs 45 | 46 | **Oddly enough**, even with `request.remote_ip` returning the correct value, Rails log will *still* contain IP addresses from your proxy servers. To fix this, you'll need to tell Rails to use a different logger. 47 | 48 | ```ruby 49 | # Add the following to config/application.rb or conifg/environments/*.rb 50 | 51 | config.middleware.insert_before(Rails::Rack::Logger, RemoteIpProxyScrubber.patched_logger) 52 | config.middleware.delete(Rails::Rack::Logger) 53 | ``` 54 | 55 | ## Other Rack Apps: Fixing `request.ip` 56 | 57 | So long as you're using Rack, you can benefit from this gem! Just add the following near the top of your `config.ru` file: 58 | 59 | ```ruby 60 | use RemoteIpProxyScrubber.filter_middleware, [ 61 | "17.0.0.4/30", 62 | "17.17.0.8/30", 63 | ] 64 | ``` 65 | 66 | # Questions? Contributions? 67 | 68 | If this gem isn't working for you, feel free to open up an Issue, or a Pull Request if you've got a proposed solution! I maintain this project in my spare time, so your patience is appreciated. 69 | 70 | # Credit 71 | 72 | Thanks to [Haiku Learning](http://www.haikulearning.com) for sponsoring the initial development of this gem. We're scratching our own itch, but hopefully it's helpful for you too! -------------------------------------------------------------------------------- /spec/remote_ip_proxy_scrubber_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../lib/remote_ip_proxy_scrubber' 2 | include SpecHelper 3 | 4 | # Define a few items that we'll stub out during out tests 5 | class Rails 6 | def self.version 7 | raise 'this method must be stubbed in our tests' 8 | end 9 | module Rack 10 | class Logger 11 | end 12 | end 13 | end 14 | 15 | # Backport the Rails methods we need for testing 16 | class Hash 17 | def reverse_merge!(other_hash) 18 | replace(other_hash.merge(self)) 19 | end 20 | end 21 | class Array 22 | def extract_options! 23 | last.is_a?(::Hash) ? pop : {} 24 | end 25 | end 26 | 27 | describe RemoteIpProxyScrubber do 28 | 29 | describe ".patched_logger" do 30 | rails_versions_to_test = { 31 | :Rails4 => %w{4.0.0 4.1.10 4.3.3 5.0.0}, 32 | :Rails3_2_9 => %w{3.2.9 3.2.29 3.99.999}, 33 | :Rails3_2_0 => %w{3.2.0 3.2.8}, 34 | :Rails3_1 => %w{3.0.6 3.0.999 3.1.0 3.1.12 3.1.999}, 35 | :Rails3_0 => %w{3.0.0 3.0.5}, 36 | } 37 | 38 | rails_versions_to_test.each do |expected_class, current_rails_versions| 39 | expected_class_str = "RemoteIpProxyScrubber::#{expected_class}::RemoteIPLogger" 40 | current_rails_versions.each do |rails_version| 41 | it "should return #{expected_class_str} for Rails #{rails_version}" do 42 | # Given 43 | expect(Rails).to receive(:version) { rails_version } 44 | 45 | # Then 46 | expect(RemoteIpProxyScrubber.patched_logger).to \ 47 | be == expected_class_str.constantize 48 | end 49 | end 50 | end 51 | 52 | rails_versions_to_fail = %w{1.0.0 1.2.6 2.0.0 2.99.99 not.a.version} 53 | 54 | rails_versions_to_fail.each do |rails_version| 55 | it "should fail for Rails #{rails_version}" do 56 | # Given 57 | expect(Rails).to receive(:version) { rails_version } 58 | 59 | # Then/When 60 | expect { 61 | RemoteIpProxyScrubber.patched_logger 62 | }.to raise_error 63 | end 64 | end 65 | 66 | it "should fail if there is no Rails version" do 67 | # Given 68 | expect(Rails).to receive(:version) { fail NoMethodError.new("No Rails") } 69 | 70 | # Then/When 71 | expect { 72 | RemoteIpProxyScrubber.patched_logger 73 | }.to raise_error 74 | end 75 | 76 | end 77 | 78 | describe ".rails_version" do 79 | it "should prefer Rails.version if available" do 80 | # Given 81 | expected_version = Gem::Version.new('0.0.1') 82 | unexpected_version = Gem::Version.new('0.0.2') 83 | expect(Rails).to receive(:version) { expected_version.to_s } 84 | redefine_const(Object, :RAILS_GEM_VERSION, unexpected_version.to_s) do 85 | 86 | # Then 87 | expect(RemoteIpProxyScrubber.rails_version).to be == expected_version 88 | end 89 | end 90 | 91 | it "should use RAILS_VERSION if Rails.version isn't available" do 92 | # Given 93 | expected_version = Gem::Version.new('0.0.1') 94 | expect(Rails).to receive(:version) { fail NoMethodError.new("No Rails") } 95 | redefine_const(Object, :RAILS_GEM_VERSION, expected_version.to_s) do 96 | 97 | # Then 98 | expect(RemoteIpProxyScrubber.rails_version).to be == expected_version 99 | end 100 | end 101 | 102 | it "should fail if it can't figure out the version" do 103 | # Given 104 | expect(Rails).to receive(:version) { nil } 105 | redefine_const(Object, :RAILS_GEM_VERSION, nil) do 106 | 107 | # Then 108 | expect { 109 | RemoteIpProxyScrubber.rails_version 110 | }.to raise_error 111 | end 112 | end 113 | end 114 | end -------------------------------------------------------------------------------- /lib/remote_ip_proxy_scrubber/remote_ip_logger.rb: -------------------------------------------------------------------------------- 1 | # Simplify monkey-patching Rails logging 2 | # so that it logs the request.remote_ip, not just the ip 3 | module RemoteIpProxyScrubber 4 | # Rails 3.3.0 through 4.x 5 | # define the log format in Rails::Rack::Logger#started_request_message 6 | # * https://github.com/rails/rails/blob/v4.2.1/railties/lib/rails/rack/logger.rb#L48-L55 7 | # * https://github.com/rails/rails/blob/v4.0.0/railties/lib/rails/rack/logger.rb#L48-L55 8 | module Rails4 9 | class RemoteIPLogger < Rails::Rack::Logger 10 | protected 11 | def started_request_message(request) 12 | 'Started %s "%s" for %s at %s' % [ 13 | request.request_method, 14 | request.filtered_path, 15 | request.remote_ip, 16 | Time.now.to_default_s ] 17 | end 18 | end 19 | end 20 | # Rails 3.2.9 through 3.2.x 21 | # use the same Rails::Rack::Logger#started_request_message 22 | # * https://github.com/rails/rails/blob/v3.2.21/railties/lib/rails/rack/logger.rb#L37-L44 23 | # * https://github.com/rails/rails/blob/v3.2.9/railties/lib/rails/rack/logger.rb#L37-L44 24 | module Rails3_2_9 25 | class RemoteIPLogger < ::RemoteIpProxyScrubber::Rails4::RemoteIPLogger 26 | end 27 | end 28 | 29 | # Rails 3.2.0 through 3.2.8 30 | # define the log format directly in in Rails::Rack::Logger#call_app 31 | # * https://github.com/rails/rails/blob/v3.2.8/railties/lib/rails/rack/logger.rb#L22-L29 32 | # * https://github.com/rails/rails/blob/v3.2.0/railties/lib/rails/rack/logger.rb#L22-L29 33 | module Rails3_2_0 34 | class RemoteIPLogger < Rails::Rack::Logger 35 | protected 36 | def call_app(env) 37 | request = ActionDispatch::Request.new(env) 38 | path = request.filtered_path 39 | Rails.logger.info "\n\nStarted #{request.request_method} \"#{path}\" for #{request.remote_ip} at #{Time.now.to_default_s}" 40 | @app.call(env) 41 | ensure 42 | ActiveSupport::LogSubscriber.flush_all! 43 | end 44 | end 45 | end 46 | 47 | # Rails 3.0.6 through 3.1.X 48 | # use the Rails::Rack::Logger#before_dispatch method 49 | # * https://github.com/rails/rails/blob/v3.1.12/railties/lib/rails/rack/logger.rb#L20-L26 50 | # * https://github.com/rails/rails/blob/v3.1.0/railties/lib/rails/rack/logger.rb#L20-L26 51 | # * https://github.com/rails/rails/blob/v3.0.20/railties/lib/rails/rack/logger.rb#L20-L26 52 | # * https://github.com/rails/rails/blob/v3.0.6/railties/lib/rails/rack/logger.rb#L20-L26 53 | module Rails3_1 54 | class RemoteIPLogger < Rails::Rack::Logger 55 | protected 56 | def before_dispatch(env) 57 | request = ActionDispatch::Request.new(env) 58 | path = request.filtered_path 59 | 60 | info "\n\nStarted #{request.request_method} \"#{path}\" " \ 61 | "for #{request.remote_ip} at #{Time.now.to_default_s}" 62 | end 63 | end 64 | end 65 | 66 | # Rails 3.0.0 through 3.0.5 67 | # also use the Rails::Rack::Logger#before_dispatch method 68 | # but use a slightly different implementation 69 | # Rails 3.0.0 through 3.0.3 actually are even more slightly different 70 | # but I don't expect the difference to affect our functionality 71 | # * https://github.com/rails/rails/blob/v3.0.5/railties/lib/rails/rack/logger.rb#L20-L26 72 | # * https://github.com/rails/rails/blob/v3.0.4/railties/lib/rails/rack/logger.rb#L20-L26 73 | # * https://github.com/rails/rails/blob/v3.0.3/railties/lib/rails/rack/logger.rb#L20-L26 74 | # * https://github.com/rails/rails/blob/v3.0.0/railties/lib/rails/rack/logger.rb#L20-L26 75 | module Rails3_0 76 | class RemoteIPLogger < Rails::Rack::Logger 77 | protected 78 | def before_dispatch(env) 79 | request = ActionDispatch::Request.new(env) 80 | path = request.fullpath 81 | 82 | info "\n\nStarted #{request.request_method} \"#{path}\" " \ 83 | "for #{request.remote_ip} at #{Time.now.to_default_s}" 84 | end 85 | end 86 | end 87 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # Conventionally, all specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 2 | # The generated `.rspec` file contains `--require spec_helper` which will cause 3 | # this file to always be loaded, without a need to explicitly require it in any 4 | # files. 5 | # 6 | # Given that it is always loaded, you are encouraged to keep this file as 7 | # light-weight as possible. Requiring heavyweight dependencies from this file 8 | # will add to the boot time of your test suite on EVERY test run, even for an 9 | # individual file that may not need all of that loaded. Instead, consider making 10 | # a separate helper file that requires the additional dependencies and performs 11 | # the additional setup, and require it from the spec files that actually need 12 | # it. 13 | # 14 | # The `.rspec` file also contains a few flags that are not defaults but that 15 | # users commonly want. 16 | 17 | begin 18 | require "codeclimate-test-reporter" 19 | CodeClimate::TestReporter.start 20 | rescue LoadError # Don't fail hard if this gem isn't installed 21 | end 22 | 23 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 24 | RSpec.configure do |config| 25 | # rspec-expectations config goes here. You can use an alternate 26 | # assertion/expectation library such as wrong or the stdlib/minitest 27 | # assertions if you prefer. 28 | config.expect_with :rspec do |expectations| 29 | # This option will default to `true` in RSpec 4. It makes the `description` 30 | # and `failure_message` of custom matchers include text for helper methods 31 | # defined using `chain`, e.g.: 32 | # be_bigger_than(2).and_smaller_than(4).description 33 | # # => "be bigger than 2 and smaller than 4" 34 | # ...rather than: 35 | # # => "be bigger than 2" 36 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 37 | end 38 | 39 | # rspec-mocks config goes here. You can use an alternate test double 40 | # library (such as bogus or mocha) by changing the `mock_with` option here. 41 | config.mock_with :rspec do |mocks| 42 | # Prevents you from mocking or stubbing a method that does not exist on 43 | # a real object. This is generally recommended, and will default to 44 | # `true` in RSpec 4. 45 | mocks.verify_partial_doubles = true 46 | end 47 | 48 | # The settings below are suggested to provide a good initial experience 49 | # with RSpec, but feel free to customize to your heart's content. 50 | =begin 51 | # These two settings work together to allow you to limit a spec run 52 | # to individual examples or groups you care about by tagging them with 53 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 54 | # get run. 55 | config.filter_run :focus 56 | config.run_all_when_everything_filtered = true 57 | 58 | # Limits the available syntax to the non-monkey patched syntax that is 59 | # recommended. For more details, see: 60 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 61 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 62 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 63 | config.disable_monkey_patching! 64 | 65 | # This setting enables warnings. It's recommended, but in some cases may 66 | # be too noisy due to issues in dependencies. 67 | config.warnings = true 68 | 69 | # Many RSpec users commonly either run the entire suite or an individual 70 | # file, and it's useful to allow more verbose output when running an 71 | # individual spec file. 72 | if config.files_to_run.one? 73 | # Use the documentation formatter for detailed output, 74 | # unless a formatter has already been configured 75 | # (e.g. via a command-line flag). 76 | config.default_formatter = 'doc' 77 | end 78 | 79 | # Print the 10 slowest examples and example groups at the 80 | # end of the spec run, to help surface which specs are running 81 | # particularly slow. 82 | config.profile_examples = 10 83 | 84 | # Run specs in random order to surface order dependencies. If you find an 85 | # order dependency and want to debug it, you can fix the order by providing 86 | # the seed, which is printed after each run. 87 | # --seed 1234 88 | config.order = :random 89 | 90 | # Seed global randomization in this process using the `--seed` CLI option. 91 | # Setting this allows you to use `--seed` to deterministically reproduce 92 | # test failures related to randomization by passing the same `--seed` value 93 | # as the one that triggered the failure. 94 | Kernel.srand config.seed 95 | =end 96 | end 97 | 98 | module SpecHelper 99 | # Sets $VERBOSE to nil for the duration of the block and back to its original value afterwards. 100 | # 101 | # silence_warnings do 102 | # value = noisy_call # no warning voiced 103 | # end 104 | # 105 | # noisy_call # warning voiced 106 | def silence_warnings 107 | old_verbose, $VERBOSE = $VERBOSE, nil 108 | yield 109 | ensure 110 | $VERBOSE = old_verbose 111 | end 112 | 113 | # Redefine a constant within this block. For Example: 114 | # class Apache 115 | # CONF_DIR = '/etc/httpd/conf' 116 | # end 117 | # 118 | # puts Apache::CONF_DIR #=> '/etc/httpd/conf' 119 | # redefine_const(Apache, :CONF_DIR, '/apache/conf') do 120 | # puts Apache::CONF_DIR #=> '/apache/conf' 121 | # end 122 | # puts Apache::CONF_DIR #=> '/etc/httpd/conf' 123 | def redefine_const(klass, name, value) 124 | old_value = klass.const_get(name) rescue nil 125 | silence_warnings { klass.const_set(name, value) } 126 | yield 127 | ensure 128 | silence_warnings { klass.const_set(name, old_value) } 129 | end 130 | end 131 | 132 | class String 133 | if RUBY_VERSION >= '2.0.0' 134 | def constantize 135 | Kernel.const_get(self) 136 | end 137 | else 138 | def constantize 139 | Object.module_eval("::#{self}", __FILE__, __LINE__) 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /spec/filter_proxy_ips_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../lib/remote_ip_proxy_scrubber/filter_proxy_ips' 2 | include SpecHelper 3 | 4 | 5 | # Returns an Array of input argument combinations that should always be tested 6 | def input_argmuent_variations 7 | return @variations if @variations 8 | 9 | @variations = [] 10 | 11 | single_args = [ 12 | '8.8.8.8', # single string 13 | '8.8.8.0/24', # single string range 14 | IPAddr.new('8.8.8.8'), # single IPAddr 15 | IPAddr.new('8.8.8.0/24'), # single IPAddr range 16 | /8.8.8.*/, # single Regexp 17 | ] 18 | 19 | single_args.each do |first_arg| 20 | @variations << [first_arg] 21 | single_args.each do |second_arg| 22 | @variations << [first_arg, second_arg] 23 | end 24 | end 25 | 26 | # Add a "crazy Array" variation 27 | @variations << [[single_args[0], [single_args[1]]]] 28 | 29 | @variations 30 | end 31 | 32 | describe RemoteIpProxyScrubber::Rails4::FilterProxyIPs do 33 | 34 | describe ".initialize" do 35 | # Test every possible variation of arguments 36 | input_argmuent_variations.each do |args| 37 | it "should set @proxies to an Array of Regexp and IPAddr, given #{args.inspect}" do 38 | proxies = RemoteIpProxyScrubber::Rails4::FilterProxyIPs.new(:app, *args).proxies 39 | expect(proxies).to be_a(Array) 40 | proxies.each do |proxy| 41 | expect(proxy).to be_a(Regexp).or be_a(IPAddr) 42 | end 43 | end 44 | end 45 | end 46 | 47 | describe ".call" do 48 | it "should remove a single IP with a simple String" do 49 | # Given 50 | app = double('app').as_null_object 51 | proxy_filter = RemoteIpProxyScrubber::Rails4::FilterProxyIPs.new(app, '17.0.0.1') 52 | 53 | # Then 54 | env = double("env") 55 | allow(env).to receive(:[]).with('HTTP_X_FORWARDED_FOR') { '8.8.8.8, 17.0.0.1' } 56 | expect(env).to receive(:[]=).with('HTTP_X_FORWARDED_FOR', '8.8.8.8') 57 | 58 | # When 59 | proxy_filter.call(env) 60 | end 61 | 62 | it "should remove multiple IPs with a simple String" do 63 | # Given 64 | app = double('app').as_null_object 65 | proxy_filter = RemoteIpProxyScrubber::Rails4::FilterProxyIPs.new(app, '17.0.0.1') 66 | 67 | # Then 68 | env = double("env") 69 | allow(env).to receive(:[]).with('HTTP_X_FORWARDED_FOR') { '8.8.8.8, 17.0.0.1, 17.0.0.2, 17.0.0.1' } 70 | expect(env).to receive(:[]=).with('HTTP_X_FORWARDED_FOR', '8.8.8.8, 17.0.0.2') 71 | 72 | # When 73 | proxy_filter.call(env) 74 | end 75 | 76 | it "should remove multiple IPs with a range String" do 77 | # Given 78 | app = double('app').as_null_object 79 | proxy_filter = RemoteIpProxyScrubber::Rails4::FilterProxyIPs.new(app, '17.0.0.0/4') 80 | 81 | # Then 82 | env = double("env") 83 | allow(env).to receive(:[]).with('HTTP_X_FORWARDED_FOR') { '8.8.8.8, 17.0.0.1, 17.0.0.2, 127.0.0.1' } 84 | expect(env).to receive(:[]=).with('HTTP_X_FORWARDED_FOR', '8.8.8.8, 127.0.0.1') 85 | 86 | # When 87 | proxy_filter.call(env) 88 | end 89 | 90 | it "should remove multiple IPs with a simple IPAddr" do 91 | # Given 92 | app = double('app').as_null_object 93 | proxy_filter = RemoteIpProxyScrubber::Rails4::FilterProxyIPs.new(app, IPAddr.new('17.0.0.1')) 94 | 95 | # Then 96 | env = double("env") 97 | allow(env).to receive(:[]).with('HTTP_X_FORWARDED_FOR') { '8.8.8.8, 17.0.0.1, 17.0.0.2, 17.0.0.1' } 98 | expect(env).to receive(:[]=).with('HTTP_X_FORWARDED_FOR', '8.8.8.8, 17.0.0.2') 99 | 100 | # When 101 | proxy_filter.call(env) 102 | end 103 | 104 | it "should remove multiple IPs with a range IPAddr" do 105 | # Given 106 | app = double('app').as_null_object 107 | proxy_filter = RemoteIpProxyScrubber::Rails4::FilterProxyIPs.new(app, IPAddr.new('17.0.0.0/4')) 108 | 109 | # Then 110 | env = double("env") 111 | allow(env).to receive(:[]).with('HTTP_X_FORWARDED_FOR') { '8.8.8.8, 17.0.0.1, 17.0.0.2, 127.0.0.1' } 112 | expect(env).to receive(:[]=).with('HTTP_X_FORWARDED_FOR', '8.8.8.8, 127.0.0.1') 113 | 114 | # When 115 | proxy_filter.call(env) 116 | end 117 | 118 | it "should remove IPs with a Regexp" do 119 | # Given 120 | app = double('app').as_null_object 121 | proxy_filter = RemoteIpProxyScrubber::Rails4::FilterProxyIPs.new(app, /^17\./) 122 | 123 | # Then 124 | env = double("env") 125 | allow(env).to receive(:[]).with('HTTP_X_FORWARDED_FOR') { '170.0.0.1, 17.0.0.1, 9.8.7.6, 17.254.0.1' } 126 | expect(env).to receive(:[]=).with('HTTP_X_FORWARDED_FOR', '170.0.0.1, 9.8.7.6') 127 | 128 | # When 129 | proxy_filter.call(env) 130 | end 131 | 132 | it "should NOT remove IPs with no proxy_matches" do 133 | # Given 134 | app = double('app').as_null_object 135 | proxy_filter = RemoteIpProxyScrubber::Rails4::FilterProxyIPs.new(app) 136 | 137 | # Then 138 | env = double("env") 139 | allow(env).to receive(:[]).with('HTTP_X_FORWARDED_FOR') { '170.0.0.1, 17.0.0.1, 9.8.7.6, 17.254.0.1' } 140 | expect(env).to receive(:[]=).with('HTTP_X_FORWARDED_FOR', '170.0.0.1, 17.0.0.1, 9.8.7.6, 17.254.0.1') 141 | 142 | # When 143 | proxy_filter.call(env) 144 | end 145 | 146 | it "should silently remove invalid IPs in the header" do 147 | # Given 148 | app = double('app').as_null_object 149 | proxy_filter = RemoteIpProxyScrubber::Rails4::FilterProxyIPs.new(app) 150 | 151 | invalid_ips = [ 152 | '127.0.0.500', 153 | '17.0.0.1/4', 154 | '17.0.0.1/500', 155 | 'not an IP', 156 | ] 157 | 158 | # Then 159 | env = double("env") 160 | invalid_ips.each do |invalid| 161 | allow(env).to receive(:[]).with('HTTP_X_FORWARDED_FOR') { "#{invalid}, 127.0.0.1" } 162 | expect(env).to receive(:[]=).with('HTTP_X_FORWARDED_FOR', '127.0.0.1') 163 | 164 | # When 165 | proxy_filter.call(env) 166 | end 167 | end 168 | end 169 | 170 | end 171 | 172 | --------------------------------------------------------------------------------