ip_filter 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -0
  4. data/CHANGELOG +29 -0
  5. data/Gemfile.lock +117 -0
  6. data/LICENSE +20 -0
  7. data/README.rdoc +232 -0
  8. data/Rakefile +6 -0
  9. data/data/geoip/country_code.yml +255 -0
  10. data/data/geoip/country_code3.yml +255 -0
  11. data/data/geoip/country_continent.yml +255 -0
  12. data/data/geoip/country_name.yml +255 -0
  13. data/data/geoip/time_zone.yml +677 -0
  14. data/lib/geoip.rb +559 -0
  15. data/lib/ip_filter.rb +100 -0
  16. data/lib/ip_filter/cache.rb +30 -0
  17. data/lib/ip_filter/cache/dallistore.rb +39 -0
  18. data/lib/ip_filter/cache/redis.rb +26 -0
  19. data/lib/ip_filter/configuration.rb +47 -0
  20. data/lib/ip_filter/controller/geo_ip_lookup.rb +78 -0
  21. data/lib/ip_filter/lookups/base.rb +60 -0
  22. data/lib/ip_filter/lookups/geoip.rb +41 -0
  23. data/lib/ip_filter/providers/max_mind.rb +52 -0
  24. data/lib/ip_filter/providers/s3.rb +51 -0
  25. data/lib/ip_filter/railtie.rb +23 -0
  26. data/lib/ip_filter/request.rb +14 -0
  27. data/lib/ip_filter/results/base.rb +39 -0
  28. data/lib/ip_filter/results/geoip.rb +19 -0
  29. data/lib/ip_filter/version.rb +3 -0
  30. data/spec/cache/dallistore_spec.rb +16 -0
  31. data/spec/cache/redis_spec.rb +56 -0
  32. data/spec/controller/ip_controller_spec.rb +56 -0
  33. data/spec/fixtures/GeoIP.dat +0 -0
  34. data/spec/fixtures/LICENSE.txt +31 -0
  35. data/spec/fixtures/country.dat +0 -0
  36. data/spec/ip_filter_spec.rb +19 -0
  37. data/spec/providers/max_mind_spec.rb +11 -0
  38. data/spec/providers/s3_spec.rb +11 -0
  39. data/spec/spec_helper.rb +40 -0
  40. data/spec/support/enable_dallistore_cache.rb +15 -0
  41. data/spec/support/enable_redis_cache.rb +15 -0
  42. metadata +85 -0
@@ -0,0 +1,100 @@
1
+ require 'ip_filter/configuration'
2
+ require 'ip_filter/cache'
3
+ require 'ip_filter/request'
4
+ require 'ip_filter/lookups/geoip'
5
+ require 'ip_filter/providers/s3'
6
+ require 'ip_filter/providers/max_mind'
7
+
8
+ module IpFilter
9
+ extend self
10
+ attr_accessor :updated_at
11
+ attr_reader :lookups, :refresh_inprogress
12
+
13
+ # Better configuration handling ( like Pros !)
14
+ class << self
15
+ attr_accessor :configuration
16
+ end
17
+
18
+ def self.configure
19
+ self.configuration ||= IpFilter::Configuration.new
20
+ yield(configuration)
21
+ end
22
+
23
+ # Back to IpFilter job
24
+
25
+ # Search for information about an address.
26
+ def search(query)
27
+ if !ip_address?(query) && query.blank?
28
+ raise ArgumentError, 'invalid address'
29
+ end
30
+
31
+ begin
32
+ get_lookup.search(query)
33
+ rescue
34
+ sleep(0.300) # wait to reload the file
35
+ get_lookup.search(query)
36
+ end
37
+ end
38
+
39
+ # The working Cache object, or +nil+ if none configured.
40
+ def cache
41
+ @cache ||= configuration.cache
42
+ end
43
+
44
+ def s3
45
+ return @s3 unless @s3.nil?
46
+ if !configuration.s3_access_key_id.nil? &&
47
+ !configuration.s3_secret_access_key.nil?
48
+ return @s3 ||= IpFilter::S3.new
49
+ end
50
+ @s3
51
+ end
52
+
53
+ def maxmind
54
+ @maxmind ||= IpFilter::Providers::MaxMind.new
55
+ end
56
+
57
+ def reference_file
58
+ configuration.geo_ip_dat
59
+ end
60
+
61
+ def database_files
62
+ Dir[configuration.data_folder + '/*.dat']
63
+ end
64
+
65
+ private
66
+
67
+ def refresh_db
68
+ configuration.update_method.call
69
+ IpFilter.cache.reset unless IpFilter.cache.nil?
70
+ @updated_at = Time.now
71
+ @lookups = IpFilter::Lookup::Geoip.new
72
+ ensure
73
+ @refresh_inprogress = false
74
+ end
75
+
76
+ # Retrieve a Lookup object from the store.
77
+ def get_lookup
78
+ @updated_at ||= Time.now
79
+ if !@refresh_inprogress && (@lookups.nil? || Time.now.to_i > (@updated_at.to_i + IpFilter.configuration.refresh_delay))
80
+ @refresh_inprogress = true
81
+ Thread.new { refresh_db }
82
+ end
83
+ @lookups ||= IpFilter::Lookup::Geoip.new
84
+ end
85
+
86
+ # Checks if value looks like an IP address.
87
+ #
88
+ # Does not check for actual validity, just the appearance of four
89
+ # dot-delimited numbers.
90
+ def ip_address?(value)
91
+ !!value.to_s.match(
92
+ %r(^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/\d{1,2}){0,1}$)
93
+ )
94
+ end
95
+ end
96
+
97
+ if defined?(Rails)
98
+ require 'ip_filter/railtie'
99
+ IpFilter::Railtie.insert
100
+ end
@@ -0,0 +1,30 @@
1
+ require 'ip_filter/cache/dallistore'
2
+ require 'ip_filter/cache/redis'
3
+
4
+ module IpFilter
5
+
6
+ # For now just a simple wrapper class for a Memcache client.
7
+ class Cache
8
+ attr_reader :cached_at
9
+ attr_reader :prefix, :store
10
+
11
+ def initialize(store, prefix = IpFilter.configuration.cache_prefix)
12
+ @store = store
13
+ @prefix = prefix
14
+ @cached_at ||= DateTime.now
15
+ end
16
+
17
+ def serialize_output(value)
18
+ if !value.nil? && value != 'null'
19
+ value = JSON.parse(value) if value.is_a? String
20
+ return OpenStruct.new(value)
21
+ end
22
+ return nil
23
+ end
24
+
25
+ # Cache key for a given URL.
26
+ def key_for(ip)
27
+ [prefix, ip].join
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,39 @@
1
+ module IpFilter
2
+ class Cache
3
+ class DalliStore < IpFilter::Cache
4
+
5
+ # Clean Cache (not available, as DalliStore cannot iterate on cache)
6
+ def reset
7
+ logger.warning "Cannot reset ip_filter cache with DalliStore, you must reset all your cache manually."
8
+ end
9
+
10
+ # Read from the Cache.
11
+ def [](ip)
12
+ result = case
13
+ when store.respond_to?(:read)
14
+ store.read key_for(ip)
15
+ when store.respond_to?(:[])
16
+ store[key_for(ip)]
17
+ when store.respond_to?(:get)
18
+ store.get key_for(ip)
19
+ end
20
+ # this method is inherited from IpFilter::Cache
21
+ serialize_output(result)
22
+ end
23
+
24
+ # Write to the Cache.
25
+ def []=(ip, value)
26
+ case
27
+ when store.respond_to?(:write)
28
+ store.write key_for(ip), value
29
+ when store.respond_to?(:[]=)
30
+ store[key_for(ip)] = value
31
+ when store.respond_to?(:set)
32
+ store.set key_for(ip), value
33
+ end
34
+ end
35
+
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ require "json"
2
+
3
+ module IpFilter
4
+ class Cache
5
+ class Redis < IpFilter::Cache
6
+
7
+ def reset
8
+ keys = store.keys("#{@prefix}*")
9
+ store.del keys unless keys.empty?
10
+ end
11
+
12
+ # Read from the Cache.
13
+ def [](ip)
14
+ value = store.get(key_for(ip))
15
+ # this method is inherited from IpFilter::Cache
16
+ serialize_output(value)
17
+ end
18
+
19
+ # Write to the Cache.
20
+ def []=(ip, value)
21
+ store.set(key_for(ip), value.to_json)
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,47 @@
1
+ module IpFilter
2
+ class Configuration
3
+ attr_accessor :data_folder, :geo_ip_dat, :geoip_level, :update_method,
4
+ :ip_code_type, :ip_codes, :ip_whitelist, :ip_exception,
5
+ :allow_loopback, :cache, :cache_prefix, :geoipupdate_config,
6
+ :s3_access_key_id, :s3_secret_access_key, :s3_bucket_name,
7
+ :refresh_delay
8
+
9
+ def initialize
10
+ # Folder containing GeoIP database files.
11
+ @data_folder = '/tmp/geoip'
12
+ # Level of filtering : Country, city...
13
+ @geo_ip_dat = 'data/GeoIP.dat'
14
+ @geoip_level = :country
15
+ # Logic to use to update geoip.dat file
16
+ @update_method = []
17
+ # Must be 'country_code', 'country_code2', 'country_code3',
18
+ # 'country_name', 'continent_code'
19
+ @ip_code_type = nil
20
+ # Must be of the corresponding format as :ip_code_type
21
+ @ip_codes = []
22
+ # Whitelist of IPs
23
+ @ip_whitelist = []
24
+ # Exceptions that should not be rescued by default
25
+ # (if you want to implement custom error handling);
26
+ @ip_exception = Exception.new
27
+ # Allow loopback Ip
28
+ @allow_loopback = true
29
+ # cache object (must respond to #[], #[]=, and #keys)
30
+ @cache = nil
31
+ # prefix (string) to use for all cache keys
32
+ @cache_prefix = 'ip_filter:'
33
+ # Configuration path for geoipupdate binary
34
+ @geoipupdate_config = '/usr/local/etc/GeoIP.conf'
35
+ ## S3 credentials ##
36
+ # if access_key_id is nil, S3 isn't loaded.
37
+ @s3_access_key_id = nil
38
+ # S3 Secret API key
39
+ @s3_secret_access_key = nil
40
+ # S3 bucket name
41
+ @s3_bucket_name = 'ip_filter-geoip'
42
+ # Cache refresh delay, every 24 hours by default
43
+ @refresh_delay = 86400
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,78 @@
1
+ module IpFilter
2
+ module Controller
3
+ module GeoIpLookup
4
+ # Mix below class methods into ActionController.
5
+ def self.included(base)
6
+ base.send :include, InstanceMethods
7
+ base.send :extend, ClassMethods
8
+ end
9
+
10
+ #
11
+ # Class methods
12
+ #
13
+ module ClassMethods
14
+ def validate_ip(filter_options = {}, &block)
15
+ if block
16
+ before_filter filter_options do |controller|
17
+ controller.check_ip_location(block)
18
+ end
19
+ else
20
+ before_filter :check_ip_location, filter_options
21
+ end
22
+ end
23
+
24
+ def skip_validate_ip(filter_options = {})
25
+ skip_before_filter(:check_ip_location, filter_options)
26
+ end
27
+
28
+ def code_type
29
+ @code_type ||= IpFilter.configuration.ip_code_type.to_sym
30
+ end
31
+
32
+ def codes
33
+ IpFilter.configuration.ip_codes
34
+ end
35
+
36
+ def whitelist
37
+ IpFilter.configuration.ip_whitelist
38
+ end
39
+
40
+ def allow_loopback?
41
+ @allow_loopback ||= IpFilter.configuration.allow_loopback
42
+ end
43
+ end
44
+
45
+ #
46
+ # Instance methods
47
+ #
48
+ module InstanceMethods
49
+ private
50
+
51
+ def check_ip_location(block = nil)
52
+ code = request.location[self.class.code_type]
53
+ ip = request.remote_ip || request.ip
54
+
55
+ perform_check = self.class.allow_loopback? ? (code != 'N/A') : true
56
+
57
+ if perform_check
58
+ unless valid_code?(code) || valid_ip?(ip)
59
+ block ? block.call : IpFilter.configuration.ip_exception.call
60
+ end
61
+ end
62
+ end
63
+
64
+ def valid_code?(code)
65
+ code.in? self.class.codes
66
+ end
67
+
68
+ def valid_ip?(ip)
69
+ # go through each IP range and validate IP against it
70
+ Array.wrap(self.class.whitelist).any? do |ip_range|
71
+ IPAddr.new(ip_range).include?(ip)
72
+ end
73
+ end
74
+ end
75
+ # end of module
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,60 @@
1
+ require 'ipaddr'
2
+
3
+ module IpFilter
4
+ module Lookup
5
+ class Base
6
+
7
+ # A number of non-routable IP ranges.
8
+ #
9
+ # --
10
+ # Sources for these:
11
+ # RFC 3330: Special-Use IPv4 Addresses
12
+ # The bogon list: http://www.cymru.com/Documents/bogon-list.html
13
+ NON_ROUTABLE_IP_RANGES = [
14
+ IPAddr.new('0.0.0.0/8'), # "This" Network
15
+ IPAddr.new('10.0.0.0/8'), # Private-Use Networks
16
+ IPAddr.new('14.0.0.0/8'), # Public-Data Networks
17
+ IPAddr.new('127.0.0.0/8'), # Loopback
18
+ IPAddr.new('169.254.0.0/16'), # Link local
19
+ IPAddr.new('172.16.0.0/12'), # Private-Use Networks
20
+ IPAddr.new('192.0.2.0/24'), # Test-Net
21
+ IPAddr.new('192.168.0.0/16'), # Private-Use Networks
22
+ IPAddr.new('198.18.0.0/15'), # Network Interconnect Device Benchmark Testing
23
+ IPAddr.new('224.0.0.0/4'), # Multicast
24
+ IPAddr.new('240.0.0.0/4') # Reserved for future use
25
+ ].freeze
26
+
27
+
28
+ # Query the GeoIP database for a given IP address, and returns information about
29
+ # the region/country where the IP address is allocated.
30
+ #
31
+ # Takes a search string (eg: "205.128.54.202") for country info
32
+ # Returns an array of <tt>IpFilter::Result</tt>s.
33
+ def search(query)
34
+ results(query).map { |r| result_class.new(r) }
35
+ end
36
+
37
+ private
38
+
39
+ # IpFilter::Result object or nil on timeout or other error.
40
+ def results(query, reverse = false)
41
+ raise NotImplementedError.new
42
+ end
43
+
44
+ # Class of the result objects.
45
+ def result_class
46
+ IpFilter::Result.const_get(self.class.to_s.split(":").last)
47
+ end
48
+
49
+ # The working Cache object.
50
+ def cache
51
+ IpFilter.cache
52
+ end
53
+
54
+ # Checks if address is a loopback/private address range.
55
+ def loopback_address?(ip)
56
+ NON_ROUTABLE_IP_RANGES.any? { |range| range.include?(ip) }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,41 @@
1
+ require 'ip_filter/lookups/base'
2
+ require 'ip_filter/results/geoip'
3
+ require 'geoip'
4
+
5
+ module IpFilter
6
+ module Lookup
7
+ class Geoip < Base
8
+ private
9
+
10
+ def fetch_data(query)
11
+ data = cache[query]
12
+ unless cache && data
13
+ data = geo_ip_lookup.country(query).to_hash
14
+ cache[query] = data if cache
15
+ end
16
+ data
17
+ end
18
+
19
+ def geo_ip_lookup
20
+ @geo_ip_lookup ||= GeoIP.new(IpFilter.reference_file)
21
+ end
22
+
23
+ def results(query)
24
+ # don't look up a loopback address, just return the stored result
25
+ return [reserved_result(query)] if loopback_address?(query)
26
+ [fetch_data(query)]
27
+ end
28
+
29
+ def reserved_result(ip)
30
+ {
31
+ ip: ip,
32
+ country_code: 'N/A',
33
+ country_code2: 'N/A',
34
+ country_code3: 'N/A',
35
+ country_name: 'N/A',
36
+ continent_code: 'N/A'
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,52 @@
1
+ require 'fileutils'
2
+
3
+ module IpFilter
4
+ module Providers
5
+ class MaxMind
6
+ attr_reader :files
7
+
8
+ def initialize
9
+ check_geoipupdate_presence!
10
+ refresh_file_list!
11
+ update! if @files.empty?
12
+ return @files
13
+ end
14
+
15
+ def update!
16
+ # Execute geoipupdate command.
17
+ if %x{geoipupdate -f #{config} -d #{folder}}
18
+ refresh_file_list!
19
+ return true
20
+ end
21
+ return false
22
+ end
23
+
24
+ def config
25
+ @config ||= IpFilter.configuration.geoipupdate_config
26
+ end
27
+
28
+ def folder
29
+ @folder ||= IpFilter.configuration.data_folder
30
+ end
31
+
32
+ def refresh_file_list!
33
+ @files = Dir["#{folder}/*.dat"]
34
+ end
35
+
36
+ protected
37
+
38
+ def check_geoipupdate_presence!
39
+ if not %x{command -v geoipupdate >/dev/null 2>&1 || { 'false'>&2; exit 1; }}
40
+ puts 'WARNING: `geoipupdate` binary is required, to setup it do the following :'
41
+ puts 'First, add the maxmind ppa repository: `add-apt-repository ppa:maxmind/ppa`'
42
+ puts 'Next, update your package list: `apt-get update`'
43
+ puts 'Finally, setup the package: `apt-get install geoipupdate`'
44
+ raise "Missing binary : geoipupdate"
45
+ end
46
+ return true
47
+ end
48
+
49
+
50
+ end
51
+ end
52
+ end