sypex_geo 0.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0b509a171c46e45958ffd1cd999fd539e7d307e0
4
+ data.tar.gz: 876b33e9d0a53db5826f9b7850dbdc66b5d616cd
5
+ SHA512:
6
+ metadata.gz: 2c6922ee63332e359bde80669f918627dfe196aa8cdf4a7b44c9014eaba363dcc6ae1a9e457e9313f0048ead8cd0b7f365f9ea0adf2b2b436eaa47a30daf308b
7
+ data.tar.gz: 20b94c1123899ced1bbe980ed16cd9e3493237d1741957ae688f988db9e69b477898e4f28ad563f36fd5a0aaf07a9f1b265781a397a3f51ad49ad7ca244bf815
data/.editorconfig ADDED
@@ -0,0 +1,10 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+
9
+ trim_trailing_whitespace = true
10
+ insert_final_newline = true
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ pkg/
3
+ .bundle
4
+ .config
5
+ Gemfile.lock
6
+ coverage
7
+ tmp
8
+ *.bundle
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format=doc
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Kolesnikov Danil
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # SypexGeo
2
+
3
+ [Sypex Geo IP database](http://sypexgeo.net) adapter for Ruby.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'sypex_geo'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install sypex_geo
18
+
19
+ ## Usage
20
+
21
+ require 'sypex_geo'
22
+
23
+ db = SypexGeo::Database.new('./sypex_geo_city_max.dat')
24
+ db.lookup(<IPv4 address>)
25
+
26
+ ## License
27
+
28
+ Licensed under the MIT License
29
+
30
+ Copyright (c) 2014 Kolesnikov Danil
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,3 @@
1
+ module SypexGeo
2
+ VERSION = '0.0.1'
3
+ end
data/lib/sypex_geo.rb ADDED
@@ -0,0 +1,261 @@
1
+ require 'ipaddr'
2
+ require 'sypex_geo/version'
3
+
4
+ module SypexGeo
5
+ TYPE_COUNTRY = 0
6
+ TYPE_REGION = 1
7
+ TYPE_CITY = 2
8
+
9
+ COUNTRY_CODES = %w[
10
+ AP EU AD AE AF AG AI AL AM AN AO AQ AR AS AT AU AW AZ BA BB BD BE BF BG
11
+ BH BI BJ BM BN BO BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN
12
+ CO CR CU CV CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM
13
+ FO FR FX GA GB GD GE GF GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN
14
+ HR HT HU ID IE IL IN IO IQ IR IS IT JM JO JP KE KG KH KI KM KN KP KR KW
15
+ KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD MG MH MK ML MM MN MO MP
16
+ MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA
17
+ PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RU RW SA SB SC SD SE SG
18
+ SH SI SJ SK SL SM SN SO SR ST SV SY SZ TC TD TF TG TH TJ TK TM TN TO TL
19
+ TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT RS ZA
20
+ ZM ME ZW A1 A2 O1 AX GG IM JE BL MF
21
+ ]
22
+
23
+ class DatabaseError < StandardError
24
+ end
25
+
26
+ class Database
27
+ attr_reader :version
28
+
29
+ def initialize(path)
30
+ @file = File.open(path, 'rb')
31
+
32
+ setup!
33
+ end
34
+
35
+ def lookup(ip, full = false)
36
+ if seek = search(ip)
37
+ read_location(seek, full)
38
+ end
39
+ end
40
+
41
+ def inspect
42
+ "#<#{self.class}:0x#{object_id} @version=#{@version}>"
43
+ end
44
+
45
+ protected
46
+
47
+ def setup!
48
+ if header = @file.read(40)
49
+ id, @version, @time, @type, @charset,
50
+ @b_idx_len, @m_idx_len, @range, @db_items, @id_len,
51
+ @max_region, @max_city, @region_size, @city_size,
52
+ @max_country, @country_size,
53
+ @pack_size = header.unpack('a3CNCCCnnNCnnNNnNn')
54
+ end
55
+
56
+ raise DatabaseError.new, 'Wrong file format' unless id == 'SxG'
57
+
58
+ @pack = @file.read(@pack_size).split("\0")
59
+ @b_idx_arr = @file.read(@b_idx_len * 4).unpack('N*')
60
+ @m_idx_arr = @file.read(@m_idx_len * 4).scan(/.{1,4}/m)
61
+
62
+ @block_len = 3 + @id_len
63
+ @db_begin = @file.tell
64
+ @regions_begin = @db_begin + @db_items * @block_len
65
+ @cities_begin = @regions_begin + @region_size
66
+ end
67
+
68
+ def search(ip)
69
+ ip1n = ip.to_i
70
+
71
+ return if ip1n == 0 or ip1n == 127 or ip1n >= 224
72
+
73
+ ipn = IPAddr.new(ip).hton
74
+ blocks_min, blocks_max = @b_idx_arr[ip1n - 1], @b_idx_arr[ip1n]
75
+
76
+ if blocks_max - blocks_min > @range
77
+ part = search_idx(ipn, blocks_min / @range, (blocks_max / @range) - 1)
78
+ min = part > 0 ? part * @range : 0
79
+ max = part > @m_idx_len ? @db_items : (part + 1) * @range
80
+ min = blocks_min if min < blocks_min
81
+ max = blocks_max if max > blocks_max
82
+ else
83
+ min = blocks_min
84
+ max = blocks_max
85
+ end
86
+
87
+ search_db(ipn, min, max)
88
+ end
89
+
90
+ def search_idx(ipn, min, max)
91
+ idx = @m_idx_arr
92
+
93
+ while max - min > 8
94
+ offset = (min + max) >> 1
95
+
96
+ if ipn > idx[offset]
97
+ min = offset
98
+ else
99
+ max = offset
100
+ end
101
+ end
102
+
103
+ while ipn > idx[min]
104
+ break if min >= max
105
+ min += 1
106
+ end
107
+
108
+ min
109
+ end
110
+
111
+ def search_db(ipn, min, max)
112
+ len = max - min
113
+ @file.pos = @db_begin + min * @block_len
114
+ search_db_chunk(@file.read(len * @block_len), ipn, 0, len - 1)
115
+ end
116
+
117
+ def search_db_chunk(data, ipn, min, max)
118
+ block_len = @block_len
119
+
120
+ if max - min > 1
121
+ ipn = ipn[1, 3]
122
+
123
+ while max - min > 8
124
+ offset = (min + max) >> 1
125
+
126
+ if ipn > data[offset * block_len, 3]
127
+ min = offset
128
+ else
129
+ max = offset
130
+ end
131
+ end
132
+
133
+ while ipn >= data[min * block_len, 3]
134
+ min += 1
135
+ break if min >= max
136
+ end
137
+ else
138
+ min += 1
139
+ end
140
+
141
+ data[min * block_len - @id_len, @id_len].unpack('H*').first.hex
142
+ end
143
+
144
+ def read_data(seek, limit, type)
145
+ @file.pos = (type == TYPE_REGION ? @regions_begin : @cities_begin) + seek
146
+ Pack.parse(@pack[type], @file.read(limit))
147
+ end
148
+
149
+ def read_country(seek)
150
+ read_data(seek, @max_country, TYPE_COUNTRY)
151
+ end
152
+
153
+ def read_region(seek)
154
+ read_data(seek, @max_region, TYPE_REGION)
155
+ end
156
+
157
+ def read_city(seek)
158
+ read_data(seek, @max_city, TYPE_CITY)
159
+ end
160
+
161
+ def read_location(seek, full = false)
162
+ region = nil
163
+ city = nil
164
+ country = nil
165
+
166
+ if seek < @country_size
167
+ country = read_country(seek)
168
+ elsif city = read_city(seek)
169
+ region_seek = city.delete(:region_seek)
170
+ country_id = city.delete(:country_id)
171
+ country = { id: country_id, iso: COUNTRY_CODES[country_id - 1] }
172
+ end
173
+
174
+ if full and region_seek
175
+ region = read_region(region_seek)
176
+ country = read_country(region.delete(:country_seek))
177
+ end
178
+
179
+ { city: city, region: region, country: country }
180
+ end
181
+ end
182
+
183
+ class MemoryDatabase < Database
184
+ def setup!
185
+ super
186
+
187
+ @db = @file.read(@db_items * @block_len)
188
+ @regions_db = @file.read(@region_size) if @region_size > 0
189
+ @cities_db = @file.read(@city_size) if @city_size > 0
190
+ end
191
+
192
+ def search_db(ipn, min, max)
193
+ search_db_chunk(@db, ipn, min, max)
194
+ end
195
+
196
+ def read_data(seek, limit, type)
197
+ raw = (type == TYPE_REGION ? @regions_db : @cities_db)[seek, limit]
198
+ Pack.parse(@pack[type], raw)
199
+ end
200
+ end
201
+
202
+ module Pack
203
+ def self.parse(pack, data)
204
+ result = {}
205
+ pos = 0
206
+
207
+ pack.split('/').each do |p|
208
+ type, name = p.split(':')
209
+
210
+ if data.nil? or data.empty?
211
+ result[name] = type[0] =~ /b|c/ ? '' : 0
212
+ else
213
+ if type[0] == 'b'
214
+ len = data.index("\0", pos) - pos
215
+ val = data[pos, len].force_encoding('UTF-8')
216
+ len += 1
217
+ else
218
+ len = type_length(type)
219
+ val = unpack(type, data[pos, len])
220
+ end
221
+
222
+ result[name] = val.is_a?(Array) ? val[0] : val
223
+ pos += len
224
+ end
225
+ end
226
+
227
+ Hash[result.map{ |k, v| [ k.to_sym, v ] }]
228
+ end
229
+
230
+ protected
231
+
232
+ def self.type_length(type)
233
+ case type[0]
234
+ when /t|T/ then 1
235
+ when /s|S|n/ then 2
236
+ when /m|M/ then 3
237
+ when 'd' then 8
238
+ when 'c' then type[1..-1].to_i
239
+ else 4
240
+ end
241
+ end
242
+
243
+ def self.unpack(type, val)
244
+ case type[0]
245
+ when 't' then val.unpack('c')
246
+ when 'T' then val.unpack('C')
247
+ when 's' then val.unpack('s')
248
+ when 'S' then val.unpack('S')
249
+ when 'm' then (val + (val[2].ord >> 7) > 0 ? "\xFF" : "\0").unpack('l')
250
+ when 'M' then (val + "\0").unpack('L')
251
+ when 'i' then val.unpack('l')
252
+ when 'I' then val.unpack('L')
253
+ when 'f' then val.unpack('f')
254
+ when 'd' then val.unpack('d')
255
+ when 'n' then val.unpack('s')[0] / (10 ** type[1].to_i)
256
+ when 'N' then val.unpack('l')[0] / (10 ** type[1].to_i)
257
+ when 'c' then val.rstrip
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,8 @@
1
+ if ENV['SIMPLECOV']
2
+ require 'simplecov'
3
+ SimpleCov.start
4
+ end
5
+
6
+ RSpec.configure do |config|
7
+ config.order = 'random'
8
+ end
@@ -0,0 +1 @@
1
+ invalid
@@ -0,0 +1,116 @@
1
+ require 'sypex_geo'
2
+
3
+ describe SypexGeo do
4
+ let(:demo_ip) do
5
+ # Random Moscow IP.
6
+ '80.90.64.1'
7
+ end
8
+
9
+ let(:default_db_file) do
10
+ File.expand_path(__FILE__ + '/../support/sypexgeo_city_max.dat')
11
+ end
12
+
13
+ let(:invalid_db_file) do
14
+ File.expand_path(__FILE__ + '/../support/invalid.dat')
15
+ end
16
+
17
+ let(:db_file) do
18
+ ENV['SYPEXGEO_CITY_MAX_DB'] || default_db_file
19
+ end
20
+
21
+ let(:city_info) do
22
+ {
23
+ city: {
24
+ id: 524901,
25
+ lat: 55,
26
+ lon: 37,
27
+ name_ru: 'Москва',
28
+ name_en: 'Moscow',
29
+ okato: '45'
30
+ },
31
+ country: {
32
+ id: 185,
33
+ iso: 'RU'
34
+ },
35
+ region: nil
36
+ }
37
+ end
38
+
39
+ let(:location_info) do
40
+ {
41
+ city: {
42
+ id: 524901,
43
+ lat: 55,
44
+ lon: 37,
45
+ name_ru: 'Москва',
46
+ name_en: 'Moscow',
47
+ okato: '45'
48
+ },
49
+ region: {
50
+ id: 524894,
51
+ name_ru: 'Москва',
52
+ name_en: 'Moskva',
53
+ lat: 55,
54
+ lon: 37,
55
+ iso: 'RU-MOW',
56
+ timezone: 'Europe/Moscow',
57
+ okato: '45'
58
+ },
59
+ country: {
60
+ id: 185,
61
+ iso: 'RU',
62
+ continent: 'EU',
63
+ lat: 60,
64
+ lon: 100,
65
+ name_ru: 'Россия',
66
+ name_en: 'Russia',
67
+ timezone: 'Europe/Moscow'
68
+ }
69
+ }
70
+ end
71
+
72
+ shared_examples 'geoip_database' do
73
+ describe '#initialize' do
74
+ it 'raises error if database is invalid' do
75
+ expect do
76
+ subject.class.new(invalid_db_file)
77
+ end.to raise_error(SypexGeo::DatabaseError)
78
+ end
79
+ end
80
+
81
+ describe '#lookup' do
82
+ it 'returns nil if IP address is reserved' do
83
+ expect(subject.lookup('0.0.0.0')).to be_nil
84
+ expect(subject.lookup('127.0.0.0')).to be_nil
85
+ expect(subject.lookup('224.0.0.0')).to be_nil
86
+ expect(subject.lookup('255.0.0.0')).to be_nil
87
+ end
88
+
89
+ it 'raises IPAddr::InvalidAddressError if IP address is invalid' do
90
+ expect do
91
+ subject.lookup('1.invalid')
92
+ end.to raise_error(IPAddr::InvalidAddressError)
93
+ end
94
+
95
+ it 'returns city info' do
96
+ expect(subject.lookup(demo_ip)).to eq(city_info)
97
+ end
98
+
99
+ it 'returns detailed location info if specified' do
100
+ expect(subject.lookup(demo_ip, true)).to eq(location_info)
101
+ end
102
+ end
103
+ end
104
+
105
+ describe SypexGeo::Database do
106
+ subject(:db) { SypexGeo::Database.new(db_file) }
107
+
108
+ it_behaves_like 'geoip_database'
109
+ end
110
+
111
+ describe SypexGeo::MemoryDatabase do
112
+ subject(:db) { SypexGeo::MemoryDatabase.new(db_file) }
113
+
114
+ it_behaves_like 'geoip_database'
115
+ end
116
+ end
data/sypex_geo.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'sypex_geo/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'sypex_geo'
8
+ spec.version = SypexGeo::VERSION
9
+ spec.authors = ['Kolesnikov Danil']
10
+ spec.email = ['kolesnikovde@gmail.com']
11
+ spec.summary = 'Sypex Geo IP database adapter for Ruby.'
12
+ spec.description = 'Sypex Geo IP database adapter for Ruby.'
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^spec/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.6'
22
+ spec.add_development_dependency 'rake'
23
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sypex_geo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Kolesnikov Danil
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Sypex Geo IP database adapter for Ruby.
42
+ email:
43
+ - kolesnikovde@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".editorconfig"
49
+ - ".gitignore"
50
+ - ".rspec"
51
+ - Gemfile
52
+ - LICENSE
53
+ - README.md
54
+ - Rakefile
55
+ - lib/sypex_geo.rb
56
+ - lib/sypex_geo/version.rb
57
+ - spec/spec_helper.rb
58
+ - spec/support/invalid.dat
59
+ - spec/sypex_geo_spec.rb
60
+ - sypex_geo.gemspec
61
+ homepage: ''
62
+ licenses:
63
+ - MIT
64
+ metadata: {}
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 2.2.2
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: Sypex Geo IP database adapter for Ruby.
85
+ test_files:
86
+ - spec/spec_helper.rb
87
+ - spec/support/invalid.dat
88
+ - spec/sypex_geo_spec.rb