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.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/CHANGELOG +29 -0
- data/Gemfile.lock +117 -0
- data/LICENSE +20 -0
- data/README.rdoc +232 -0
- data/Rakefile +6 -0
- data/data/geoip/country_code.yml +255 -0
- data/data/geoip/country_code3.yml +255 -0
- data/data/geoip/country_continent.yml +255 -0
- data/data/geoip/country_name.yml +255 -0
- data/data/geoip/time_zone.yml +677 -0
- data/lib/geoip.rb +559 -0
- data/lib/ip_filter.rb +100 -0
- data/lib/ip_filter/cache.rb +30 -0
- data/lib/ip_filter/cache/dallistore.rb +39 -0
- data/lib/ip_filter/cache/redis.rb +26 -0
- data/lib/ip_filter/configuration.rb +47 -0
- data/lib/ip_filter/controller/geo_ip_lookup.rb +78 -0
- data/lib/ip_filter/lookups/base.rb +60 -0
- data/lib/ip_filter/lookups/geoip.rb +41 -0
- data/lib/ip_filter/providers/max_mind.rb +52 -0
- data/lib/ip_filter/providers/s3.rb +51 -0
- data/lib/ip_filter/railtie.rb +23 -0
- data/lib/ip_filter/request.rb +14 -0
- data/lib/ip_filter/results/base.rb +39 -0
- data/lib/ip_filter/results/geoip.rb +19 -0
- data/lib/ip_filter/version.rb +3 -0
- data/spec/cache/dallistore_spec.rb +16 -0
- data/spec/cache/redis_spec.rb +56 -0
- data/spec/controller/ip_controller_spec.rb +56 -0
- data/spec/fixtures/GeoIP.dat +0 -0
- data/spec/fixtures/LICENSE.txt +31 -0
- data/spec/fixtures/country.dat +0 -0
- data/spec/ip_filter_spec.rb +19 -0
- data/spec/providers/max_mind_spec.rb +11 -0
- data/spec/providers/s3_spec.rb +11 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/support/enable_dallistore_cache.rb +15 -0
- data/spec/support/enable_redis_cache.rb +15 -0
- metadata +85 -0
data/lib/ip_filter.rb
ADDED
@@ -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
|