geolocal 0.5 → 0.6.1

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 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