ip_filter 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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