geolocal 0.5 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 42ed22b2aefba01694af9ec4f3fb0f969f307ef8
4
- data.tar.gz: 969d5d208bb9fd73e83d363c2b628a370af600b7
3
+ metadata.gz: 94b1ea23b169d938aedf7afa726b35f8c59025e7
4
+ data.tar.gz: 7bde790f3742741c3ea0f94f0640927f93c02b6c
5
5
  SHA512:
6
- metadata.gz: 319338e43dac19ca42ef0e7d6d3ff59e4862a71f95b1e6f34f17f1b96675bbf966e4d190c68b26c9342ff3cc1bd1f3efc3c1287ea053f316e57b4e9653948490
7
- data.tar.gz: c36302f7fa49e21a064410754030276cc2e71398618eca05dad5d0bfacfd11285a61b173627f10c6547a9ad57c0cb01b4aaaaa8c3e1205ef32883afdbb833b25
6
+ metadata.gz: 5c3267bab152d3583da3aeedb7d2ba1467c9d3762061392d972db95974a5297a56f03056d96e7274c2897bd0f98dc7174710a7e67570c90eddfd26ef38131569
7
+ data.tar.gz: 9427d86aa844125c4259c356801a3ad10716651b0b825306af08e3c3eb289d59fd98bc29e530526b72a0302278aa0a425d2e0b37284762fbfba5ff3183ee0974
data/README.md CHANGED
@@ -36,18 +36,21 @@ Geolocal.in_us?(request.remote_ip)
36
36
  Geolocal.in_central_america?(IPAddr.new('200.16.66.0'))
37
37
  ```
38
38
 
39
+ You can pass:
40
+ * a string: `Geolocal.in_us?("10.1.2.3")`
41
+ * an [IPAddr](http://www.ruby-doc.org/stdlib-2.2.0/libdoc/ipaddr/rdoc/IPAddr.html) object:
42
+ `Geolocal.in_eu?(IPAddr.new('2.16.54.0'))`
43
+ * an integer/family combo: `Geolocal.in_us?(167838211, Socket::AF_INET)`
39
44
 
40
45
  ## Config
41
46
 
42
- Here are the most common config keys. See the docs for the provider
43
- you're using for more.
47
+ Here are the supported configuration options:
44
48
 
45
49
  * **provider**: Where to download the geocoding data. Default: DB_IP.
46
50
  * **module**: The name of the module to receive the `in_*` methods. Default: 'Geolocal'.
47
51
  * **file**: Path to the file to contain the generated code. Default: `lib/#{module}.rb`.
48
52
  * **tmpdir**: the directory to contain intermediate files. They will require tens of megabytes
49
53
  for countries, hundreds for cities). Default: `./tmp/geolocal`
50
- * **expires**: the amount of time to consider downloaded data valid. Default: 1.month
51
54
  * **countries**: the ISO-codes of the countries to include in the lookup.
52
55
  * **ipv6**: whether the ranges should support ipv6 addresses.
53
56
 
@@ -67,8 +70,8 @@ Geolocal.configure do |config|
67
70
  end
68
71
  ```
69
72
 
70
- Now you can call `Geolocal.in_eu?(ip)`. If the European Union ever changes,
71
- run `bundle update countries` and then `rake geolocal`.
73
+ Now you can call `Geolocal.in_eu?(ip)`. If the European Union membership ever changes,
74
+ run `bundle update countries` and then `rake geolocal` to bring your app up to date.
72
75
 
73
76
  ## Providers
74
77
 
@@ -88,12 +91,11 @@ environments like Heroku.
88
91
 
89
92
  ## TODO
90
93
 
91
- - [ ] performance information? benchmarks. space saving by going ipv4-only?
92
94
  - [ ] include a Rails generator for the config file?
93
- - [ ] write a command that takes the config on the command line and writes the result to stdout?
95
+ - [ ] performance information? benchmarks. space saving by going ipv4-only?
94
96
  - [ ] Add support for cities
95
- - [ ] replace Nokogiri dependency with a single regex? Shame to force that dependency on all clients.
96
97
  - [ ] other sources for this data? [MainFacts](http://mainfacts.com/ip-address-space-addresses), [NirSoft](http://www.nirsoft.net/countryip/)
98
+ Also maybe allow providers to accept their own options?
97
99
  - [ ] Add support for for-pay features like lat/lon and timezones?
98
100
 
99
101
 
@@ -17,9 +17,6 @@ Gem::Specification.new do |spec|
17
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
18
  spec.require_paths = ["lib"]
19
19
 
20
- # hm... this is only required when running 'rake download'.
21
- spec.add_runtime_dependency "nokogiri"
22
-
23
20
  spec.add_development_dependency "bundler"
24
21
  spec.add_development_dependency "rake"
25
22
  spec.add_development_dependency "rspec"
@@ -1,5 +1,5 @@
1
1
  class Configuration
2
- OPTIONS = [ :provider, :module, :file, :tmpdir, :expires, :ipv6, :quiet, :countries ]
2
+ OPTIONS = [ :provider, :module, :file, :tmpdir, :ipv4, :ipv6, :quiet, :countries ]
3
3
  attr_accessor(*OPTIONS)
4
4
 
5
5
  def initialize
@@ -8,7 +8,7 @@ class Configuration
8
8
  @module = 'Geolocal'
9
9
  @file = nil # default is computed
10
10
  @tmpdir = 'tmp/geolocal'
11
- @expires = nil # provider chooses the most sensible
11
+ @ipv4 = true
12
12
  @ipv6 = true
13
13
  @quiet = false
14
14
  @countries = {}
@@ -13,20 +13,39 @@ module Geolocal
13
13
  end
14
14
 
15
15
  def download
16
- # TODO: skip download if local files are new enough
17
- # TODO: provide a FORCE argument to force download anyway
18
16
  download_files
19
17
  end
20
18
 
21
19
  def update
22
- countries = config[:countries].merge(config[:countries]) { |name, country_codes|
23
- Array(country_codes).map(&:upcase).to_set
20
+ countries = config[:countries].reduce({}) { |a, (k, v)|
21
+ a.merge! k.to_s.upcase => Array(v).map(&:upcase).to_set
24
22
  }
25
23
 
26
- results = countries.merge(countries) { "" }
24
+ ipv4 = Socket::AF_INET if config[:ipv4]
25
+ ipv6 = Socket::AF_INET6 if config[:ipv6]
27
26
 
28
- read_ranges(countries) do |name,lo,hi|
29
- results[name] << "#{IPAddr.new(lo).to_i}..#{IPAddr.new(hi).to_i},\n"
27
+ results = countries.keys.reduce({}) { |a, k|
28
+ a.merge! k.upcase+'v4' => '' if ipv4
29
+ a.merge! k.upcase+'v6' => '' if ipv6
30
+ a
31
+ }
32
+
33
+ read_ranges(countries) do |name,lostr,histr|
34
+ loaddr = IPAddr.new(lostr)
35
+ hiaddr = IPAddr.new(histr)
36
+ lofam = loaddr.family
37
+ hifam = hiaddr.family
38
+ raise "#{lostr} is family #{lofam} but #{histr} is #{hifam}" if lofam != hifam
39
+
40
+ if lofam == ipv4
41
+ namefam = name+'v4'
42
+ elsif lofam == ipv6
43
+ namefam = name+'v6'
44
+ else
45
+ raise "unknown family #{lofam} for #{lostr}"
46
+ end
47
+
48
+ results[namefam] << "#{loaddr.to_i}..#{hiaddr.to_i},\n"
30
49
  end
31
50
 
32
51
  File.open(config[:file], 'w') do |file|
@@ -36,15 +55,24 @@ module Geolocal
36
55
  status "done, result in #{config[:file]}\n"
37
56
  end
38
57
 
58
+ def up_to_date?(file, expiry)
59
+ return false unless File.exist?(file)
60
+ diff = Time.now - File.mtime(file)
61
+ if diff < expiry
62
+ status "using #{file} since it's #{diff.round} seconds old\n"
63
+ return true
64
+ end
65
+ end
66
+
39
67
  def output file, results
40
- names = results.keys
41
68
  modname = config[:module]
42
69
 
43
- write_header file, names, modname
70
+ write_header file, modname
44
71
 
45
- file.write "module #{modname}\n"
46
- names.each do |name|
47
- write_method file, name
72
+ config[:countries].keys.each do |name|
73
+ v4mod = config[:ipv4] ? name.to_s.upcase + 'v4' : 'nil'
74
+ v6mod = config[:ipv6] ? name.to_s.upcase + 'v6' : 'nil'
75
+ write_method file, name, v4mod, v6mod
48
76
  end
49
77
  file.write "end\n\n"
50
78
 
@@ -57,28 +85,40 @@ module Geolocal
57
85
  end
58
86
 
59
87
 
60
- def write_header file, names, modname
88
+ def write_header file, modname
61
89
  file.write <<EOL
62
- # This file is autogenerated
63
-
64
- # Defines #{names.map { |n| "#{modname}.in_#{n}?" }.join(', ')}
65
- # and #{names.map { |n| "#{modname}::#{n.upcase}" }.join(', ')}
90
+ # This file is autogenerated by the Geolocal gem
91
+
92
+ module #{modname}
93
+
94
+ def self.search address, family=nil, v4module, v6module
95
+ address = IPAddr.new(address) if address.is_a?(String)
96
+ family = address.family unless family
97
+ num = address.to_i
98
+ case family
99
+ when Socket::AF_INET then mod = v4module
100
+ when Socket::AF_INET6 then mod = v6module
101
+ else raise "Unknown family \#{family} for address \#{address}"
102
+ end
103
+ raise "ipv\#{family == 2 ? 4 : 6} was not compiled in" unless mod
104
+ true if mod.bsearch { |range| num > range.max ? 1 : num < range.min ? -1 : 0 }
105
+ end
66
106
 
67
107
  EOL
68
108
  end
69
109
 
70
- def write_method file, name
110
+ def write_method file, name, v4mod, v6mod
71
111
  file.write <<EOL
72
- def self.in_#{name}? addr
73
- num = addr.to_i
74
- #{name.upcase}.bsearch { |range| num > range.max ? 1 : num < range.min ? -1 : 0 }
112
+ def self.in_#{name}? address, family=nil
113
+ search address, family, #{v4mod}, #{v6mod}
75
114
  end
115
+
76
116
  EOL
77
117
  end
78
118
 
79
119
  def write_ranges file, modname, name, body
80
120
  file.write <<EOL
81
- #{modname}::#{name.upcase} = [
121
+ #{modname}::#{name} = [
82
122
  #{body}]
83
123
 
84
124
  EOL
@@ -3,8 +3,6 @@ require 'net/http'
3
3
  require 'fileutils'
4
4
  require 'zlib'
5
5
 
6
- require 'nokogiri'
7
-
8
6
 
9
7
  class Geolocal::Provider::DB_IP < Geolocal::Provider::Base
10
8
  START_URL = 'https://db-ip.com/db/download/country'
@@ -34,9 +32,19 @@ class Geolocal::Provider::DB_IP < Geolocal::Provider::Base
34
32
  end
35
33
 
36
34
  def download_files
35
+ # they update the file every month but no idea which day they upload it
36
+ return if up_to_date?(csv_file, 86400)
37
+
37
38
  page = Net::HTTP.get(URI START_URL)
38
- doc = Nokogiri::HTML(page)
39
- href = URI.parse doc.css('a.btn-primary').attr('href').to_s
39
+
40
+ # if we used Nokogiri: (we don't since we don't want to force the dependency)
41
+ # doc = Nokogiri::HTML(page)
42
+ # href = URI doc.css('a.btn-primary').attr('href').to_s
43
+
44
+ elem = page.match(/<a\b[^>]*class=['"][^'"]*btn-primary[^>]*>/) or
45
+ raise "no <a class='btn-primary'> element found in #{START_URL}"
46
+ attr = elem.to_s.match(/href=['"]([^'"]+)['"]/) or raise "no href found in #{elem}"
47
+ href = URI attr[1]
40
48
 
41
49
  # stream result because it's large
42
50
  FileUtils.mkdir_p(config[:tmpdir])
@@ -1,3 +1,3 @@
1
1
  module Geolocal
2
- VERSION = "0.5"
2
+ VERSION = "0.6.1"
3
3
  end
@@ -15,7 +15,7 @@ describe "configuration" do
15
15
  expect(defaults.module).to eq 'Geolocal'
16
16
  expect(defaults.file).to eq 'lib/geolocal.rb'
17
17
  expect(defaults.tmpdir).to eq 'tmp/geolocal'
18
- expect(defaults.expires).to eq nil
18
+ expect(defaults.ipv4).to eq true
19
19
  expect(defaults.ipv6).to eq true
20
20
  expect(defaults.quiet).to eq false
21
21
  expect(defaults.countries).to eq({})
@@ -67,7 +67,7 @@ describe "configuration" do
67
67
  module: 'Geolocal',
68
68
  file: 'lib/geolocal.rb',
69
69
  tmpdir: 'tmp/geolocal',
70
- expires: nil,
70
+ ipv4: true,
71
71
  ipv6: true,
72
72
  quiet: true,
73
73
  countries: {}
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+ require 'geolocal/provider/base'
3
+
4
+
5
+ describe Geolocal::Provider::Base do
6
+ let(:it) { described_class }
7
+ let(:provider) { it.new }
8
+
9
+ let(:example_results) { {
10
+ 'USv4' => "0..16777215,\n34603520..34604031,\n34605568..34606591,\n",
11
+ 'USv6' => "0..42540528726795050063891204319802818559,\n" +
12
+ "42540569291614257367232052214305390592..42540570559264857595461453711008595967,\n"
13
+ } }
14
+
15
+ before do
16
+ Geolocal.configure do |config|
17
+ config.countries = { 'us': 'US' }
18
+ end
19
+ end
20
+
21
+ it 'can generate both ipv4 and ipv6' do
22
+ Geolocal.configure do |config|
23
+ config.module = 'Geolocal_v4v6'
24
+ end
25
+
26
+ io = StringIO.new
27
+ provider.output io, example_results
28
+ expect(io.string).to include '0..42540528726795050063891204319802818559'
29
+ expect(io.string).to include '34605568..34606591'
30
+
31
+ eval io.string
32
+ expect(Geolocal_v4v6.in_us? '2.16.13.255').to eq true
33
+ expect(Geolocal_v4v6.in_us? IPAddr.new('2.16.13.255')).to eq true
34
+ expect(Geolocal_v4v6.in_us? 34606591, Socket::AF_INET).to eq true
35
+ expect(Geolocal_v4v6.in_us? 34606592, Socket::AF_INET).to eq nil
36
+
37
+ expect(Geolocal_v4v6.in_us? '2001:400::').to eq true
38
+ expect(Geolocal_v4v6.in_us? IPAddr.new('2001:400::')).to eq true
39
+ expect(Geolocal_v4v6.in_us? 42540570559264857595461453711008595967, Socket::AF_INET6).to eq true
40
+ expect(Geolocal_v4v6.in_us? 42540570559264857595461453711008595968, Socket::AF_INET6).to eq nil
41
+ end
42
+
43
+ it 'can turn off ipv4' do
44
+ Geolocal.configure do |config|
45
+ config.ipv4 = false
46
+ config.module = 'Geolocal_v6'
47
+ end
48
+
49
+ io = StringIO.new
50
+ provider.output io, example_results.tap { |h| h.delete('USv4') }
51
+ expect(io.string).to include '0..42540528726795050063891204319802818559'
52
+ expect(io.string).not_to include '34605568..34606591'
53
+
54
+ eval io.string
55
+ expect{ Geolocal_v6.in_us? '2.16.13.255' }.to raise_error(/ipv4 was not compiled in/)
56
+ expect( Geolocal_v6.in_us? '2001:400::' ).to eq true
57
+ end
58
+
59
+ it 'can turn off ipv6' do
60
+ Geolocal.configure do |config|
61
+ config.ipv6 = false
62
+ config.module = 'Geolocal_v4'
63
+ end
64
+
65
+ io = StringIO.new
66
+ provider.output io, example_results.tap { |h| h.delete('USv6') }
67
+ expect(io.string).to include '34605568..34606591'
68
+ expect(io.string).not_to include '0..42540528726795050063891204319802818559'
69
+
70
+ eval io.string
71
+ expect{ Geolocal_v4.in_us? '2001:400::' }.to raise_error(/ipv6 was not compiled in/)
72
+ expect( Geolocal_v4.in_us? '2.16.13.255' ).to eq true
73
+ end
74
+
75
+ it 'can turn off both ipv4 and ipv6' do
76
+ Geolocal.configure do |config|
77
+ config.ipv4 = false
78
+ config.ipv6 = false
79
+ end
80
+
81
+ io = StringIO.new
82
+ provider.output io, example_results.tap { |h| h.delete('USv4'); h.delete('USv6') }
83
+ expect(io.string).not_to include '34605568..34606591'
84
+ expect(io.string).not_to include '0..42540528726795050063891204319802818559'
85
+ end
86
+ end
@@ -36,6 +36,10 @@ describe Geolocal::Provider::DB_IP do
36
36
  end
37
37
 
38
38
  it 'can download the csv' do
39
+ Geolocal.configure do |config|
40
+ config.tmpdir = 'tmp/geolocal-test'
41
+ end
42
+
39
43
  # wow!! can't do this in an around hook because it gets the ordering completely wrong.
40
44
  # since around hooks wrap ALL before hooks, they end up using the previous test's config.
41
45
  if File.exist?(provider.csv_file)
@@ -53,30 +57,47 @@ describe Geolocal::Provider::DB_IP do
53
57
  describe 'generating' do
54
58
  let(:example_output) {
55
59
  <<EOL
56
- # This file is autogenerated
57
-
58
- # Defines Geolocal.in_us?, Geolocal.in_au?
59
- # and Geolocal::US, Geolocal::AU
60
+ # This file is autogenerated by the Geolocal gem
60
61
 
61
62
  module Geolocal
62
- def self.in_us? addr
63
- num = addr.to_i
64
- US.bsearch { |range| num > range.max ? 1 : num < range.min ? -1 : 0 }
63
+
64
+ def self.search address, family=nil, v4module, v6module
65
+ address = IPAddr.new(address) if address.is_a?(String)
66
+ family = address.family unless family
67
+ num = address.to_i
68
+ case family
69
+ when Socket::AF_INET then mod = v4module
70
+ when Socket::AF_INET6 then mod = v6module
71
+ else raise "Unknown family \#{family} for address \#{address}"
72
+ end
73
+ raise "ipv\#{family == 2 ? 4 : 6} was not compiled in" unless mod
74
+ true if mod.bsearch { |range| num > range.max ? 1 : num < range.min ? -1 : 0 }
75
+ end
76
+
77
+ def self.in_us? address, family=nil
78
+ search address, family, USv4, USv6
65
79
  end
66
- def self.in_au? addr
67
- num = addr.to_i
68
- AU.bsearch { |range| num > range.max ? 1 : num < range.min ? -1 : 0 }
80
+
81
+ def self.in_au? address, family=nil
82
+ search address, family, AUv4, AUv6
69
83
  end
84
+
70
85
  end
71
86
 
72
- Geolocal::US = [
87
+ Geolocal::USv4 = [
73
88
  0..16777215,
74
89
  ]
75
90
 
76
- Geolocal::AU = [
91
+ Geolocal::USv6 = [
92
+ ]
93
+
94
+ Geolocal::AUv4 = [
77
95
  16777216..16777471,
78
96
  ]
79
97
 
98
+ Geolocal::AUv6 = [
99
+ ]
100
+
80
101
  EOL
81
102
  }
82
103
 
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: geolocal
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.5'
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Bronson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-02-04 00:00:00.000000000 Z
11
+ date: 2015-02-05 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: nokogiri
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: bundler
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -107,6 +93,7 @@ files:
107
93
  - lib/tasks/geolocal.rake
108
94
  - spec/configuration_spec.rb
109
95
  - spec/data/dbip-country.csv.gz
96
+ - spec/provider/base_spec.rb
110
97
  - spec/provider/db_ip_spec.rb
111
98
  - spec/spec_helper.rb
112
99
  homepage: http://github.com/bronson/geolocal
@@ -136,5 +123,6 @@ summary: Generates plain Ruby if statements to geocode IP addresses
136
123
  test_files:
137
124
  - spec/configuration_spec.rb
138
125
  - spec/data/dbip-country.csv.gz
126
+ - spec/provider/base_spec.rb
139
127
  - spec/provider/db_ip_spec.rb
140
128
  - spec/spec_helper.rb