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/geoip.rb
ADDED
@@ -0,0 +1,559 @@
|
|
1
|
+
#
|
2
|
+
# Native Ruby reader for the GeoIP database
|
3
|
+
# Lookup the country where IP address is allocated
|
4
|
+
#
|
5
|
+
# = COPYRIGHT
|
6
|
+
#
|
7
|
+
# This version Copyright (C) 2005 Clifford Heath
|
8
|
+
# Derived from the C version, Copyright (C) 2003 MaxMind LLC
|
9
|
+
#
|
10
|
+
# This library is free software; you can redistribute it and/or
|
11
|
+
# modify it under the terms of the GNU General Public
|
12
|
+
# License as published by the Free Software Foundation; either
|
13
|
+
# version 2.1 of the License, or (at your option) any later version.
|
14
|
+
#
|
15
|
+
# This library is distributed in the hope that it will be useful,
|
16
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
17
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
18
|
+
# General Public License for more details.
|
19
|
+
#
|
20
|
+
# You should have received a copy of the GNU General Public
|
21
|
+
# License along with this library; if not, write to the Free Software
|
22
|
+
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
23
|
+
#
|
24
|
+
# = SYNOPSIS
|
25
|
+
#
|
26
|
+
# require 'geoip'
|
27
|
+
# p GeoIP.new('/usr/share/GeoIP/GeoIP.dat').country("www.netscape.sk")
|
28
|
+
#
|
29
|
+
# = DESCRIPTION
|
30
|
+
#
|
31
|
+
# GeoIP searches a GeoIP database for a given host or IP address, and
|
32
|
+
# returns information about the country where the IP address is allocated.
|
33
|
+
#
|
34
|
+
# = PREREQUISITES
|
35
|
+
#
|
36
|
+
# You need at least the free GeoIP.dat, for which the last known download
|
37
|
+
# location is <http://www.maxmind.com/download/geoip/database/GeoIP.dat.gz>
|
38
|
+
# This API requires the file to be decompressed for searching. Other versions
|
39
|
+
# of this database are available for purchase which contain more detailed
|
40
|
+
# information, but this information is not returned by this implementation.
|
41
|
+
# See www.maxmind.com for more information.
|
42
|
+
#
|
43
|
+
|
44
|
+
require 'thread' # Needed for Mutex
|
45
|
+
require 'socket'
|
46
|
+
begin
|
47
|
+
require 'io/extra' # for IO.pread
|
48
|
+
rescue LoadError
|
49
|
+
# oh well, hope they're not forking after initializing
|
50
|
+
end
|
51
|
+
|
52
|
+
require 'yaml'
|
53
|
+
|
54
|
+
class GeoIP
|
55
|
+
|
56
|
+
# The GeoIP GEM version number
|
57
|
+
VERSION = '1.1.2'
|
58
|
+
|
59
|
+
# The +data/+ directory for geoip
|
60
|
+
DATA_DIR = File.expand_path(
|
61
|
+
File.join(File.dirname(__FILE__), '..', 'data', 'geoip')
|
62
|
+
)
|
63
|
+
|
64
|
+
# Ordered list of the ISO3166 2-character country codes, ordered by
|
65
|
+
# GeoIP ID
|
66
|
+
CountryCode = YAML.load_file(File.join(DATA_DIR, 'country_code.yml'))
|
67
|
+
|
68
|
+
# Ordered list of the ISO3166 3-character country codes, ordered by
|
69
|
+
# GeoIP ID
|
70
|
+
CountryCode3 = YAML.load_file(File.join(DATA_DIR, 'country_code3.yml'))
|
71
|
+
|
72
|
+
# Ordered list of the English names of the countries, ordered by GeoIP ID
|
73
|
+
CountryName = YAML.load_file(File.join(DATA_DIR, 'country_name.yml'))
|
74
|
+
|
75
|
+
# Ordered list of the ISO3166 2-character continent code of the countries,
|
76
|
+
# ordered by GeoIP ID
|
77
|
+
CountryContinent = YAML.load_file(File.join(DATA_DIR, 'country_continent.yml'))
|
78
|
+
|
79
|
+
# Hash of the timezone codes mapped to timezone name, per zoneinfo
|
80
|
+
TimeZone = YAML.load_file(File.join(DATA_DIR, 'time_zone.yml'))
|
81
|
+
|
82
|
+
GEOIP_COUNTRY_EDITION = 1
|
83
|
+
GEOIP_CITY_EDITION_REV1 = 2
|
84
|
+
GEOIP_REGION_EDITION_REV1 = 3
|
85
|
+
GEOIP_ISP_EDITION = 4
|
86
|
+
GEOIP_ORG_EDITION = 5
|
87
|
+
GEOIP_CITY_EDITION_REV0 = 6
|
88
|
+
GEOIP_REGION_EDITION_REV0 = 7
|
89
|
+
GEOIP_PROXY_EDITION = 8
|
90
|
+
GEOIP_ASNUM_EDITION = 9
|
91
|
+
GEOIP_NETSPEED_EDITION = 10
|
92
|
+
|
93
|
+
COUNTRY_BEGIN = 16_776_960 #:nodoc:
|
94
|
+
STATE_BEGIN_REV0 = 16_700_000 #:nodoc:
|
95
|
+
STATE_BEGIN_REV1 = 16_000_000 #:nodoc:
|
96
|
+
STRUCTURE_INFO_MAX_SIZE = 20 #:nodoc:
|
97
|
+
DATABASE_INFO_MAX_SIZE = 100 #:nodoc:
|
98
|
+
MAX_ORG_RECORD_LENGTH = 300 #:nodoc:
|
99
|
+
MAX_ASN_RECORD_LENGTH = 300 #:nodoc: unverified
|
100
|
+
US_OFFSET = 1 #:nodoc:
|
101
|
+
CANADA_OFFSET = 677 #:nodoc:
|
102
|
+
WORLD_OFFSET = 1353 #:nodoc:
|
103
|
+
FIPS_RANGE = 360 #:nodoc:
|
104
|
+
FULL_RECORD_LENGTH = 50 #:nodoc:
|
105
|
+
|
106
|
+
STANDARD_RECORD_LENGTH = 3 #:nodoc:
|
107
|
+
SEGMENT_RECORD_LENGTH = 3 #:nodoc:
|
108
|
+
|
109
|
+
class Country < Struct.new(:request, :ip, :country_code, :country_code2, :country_code3, :country_name, :continent_code)
|
110
|
+
def to_hash
|
111
|
+
Hash[each_pair.to_a].each_with_object({}) { |(k,v), h| h[k.to_sym] = v }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class City < Struct.new(:request, :ip, :country_code2, :country_code3, :country_name, :continent_code,
|
116
|
+
:region_name, :city_name, :postal_code, :latitude, :longitude, :dma_code, :area_code, :timezone)
|
117
|
+
def to_hash
|
118
|
+
Hash[each_pair.to_a].each_with_object({}) { |(k,v), h| h[k.to_sym] = v }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class ASN < Struct.new(:number, :asn)
|
123
|
+
alias as_num number
|
124
|
+
end
|
125
|
+
|
126
|
+
# The Edition number that identifies which kind of database you've opened
|
127
|
+
attr_reader :database_type
|
128
|
+
|
129
|
+
alias databaseType database_type
|
130
|
+
|
131
|
+
# Open the GeoIP database and determine the file format version.
|
132
|
+
#
|
133
|
+
# +filename+ is a String holding the path to the GeoIP.dat file
|
134
|
+
# +options+ is an integer holding caching flags (unimplemented)
|
135
|
+
#
|
136
|
+
def initialize(filename, flags = 0)
|
137
|
+
@mutex = Mutex.new unless IO.respond_to?(:pread)
|
138
|
+
|
139
|
+
@flags = flags
|
140
|
+
@database_type = GEOIP_COUNTRY_EDITION
|
141
|
+
@record_length = STANDARD_RECORD_LENGTH
|
142
|
+
@file = File.open(filename, 'rb')
|
143
|
+
|
144
|
+
detect_database_type!
|
145
|
+
end
|
146
|
+
|
147
|
+
# Search the GeoIP database for the specified host, returning country
|
148
|
+
# info.
|
149
|
+
#
|
150
|
+
# +hostname+ is a String holding the host's DNS name or numeric IP
|
151
|
+
# address.
|
152
|
+
#
|
153
|
+
# If the database is a City database (normal), return the result that
|
154
|
+
# +city+ would return.
|
155
|
+
#
|
156
|
+
# Otherwise, return a Country object with the seven elements:
|
157
|
+
# * The host or IP address string as requested
|
158
|
+
# * The IP address string after looking up the host
|
159
|
+
# * The GeoIP country-ID as an integer (N.B. this is excluded from the
|
160
|
+
# city results!)
|
161
|
+
# * The two-character country code (ISO 3166-1 alpha-2)
|
162
|
+
# * The three-character country code (ISO 3166-2 alpha-3)
|
163
|
+
# * The ISO 3166 English-language name of the country
|
164
|
+
# * The two-character continent code
|
165
|
+
#
|
166
|
+
def country(hostname)
|
167
|
+
if @database_type == GEOIP_CITY_EDITION_REV0 ||
|
168
|
+
@database_type == GEOIP_CITY_EDITION_REV1
|
169
|
+
return city(hostname)
|
170
|
+
end
|
171
|
+
|
172
|
+
ip = lookup_ip(hostname)
|
173
|
+
|
174
|
+
# Convert numeric IP address to an integer
|
175
|
+
ipnum = iptonum(ip)
|
176
|
+
|
177
|
+
if @database_type != GEOIP_COUNTRY_EDITION &&
|
178
|
+
@database_type != GEOIP_PROXY_EDITION &&
|
179
|
+
@database_type != GEOIP_NETSPEED_EDITION
|
180
|
+
throw "Invalid GeoIP database type, can't look up Country by IP"
|
181
|
+
end
|
182
|
+
|
183
|
+
code = (seek_record(ipnum) - COUNTRY_BEGIN)
|
184
|
+
|
185
|
+
Country.new(
|
186
|
+
hostname, # Requested hostname
|
187
|
+
ip, # Ip address as dotted quad
|
188
|
+
code, # GeoIP's country code
|
189
|
+
CountryCode[code], # ISO3166-1 alpha-2 code
|
190
|
+
CountryCode3[code], # ISO3166-2 alpha-3 code
|
191
|
+
CountryName[code], # Country name, per ISO 3166
|
192
|
+
CountryContinent[code] # Continent code.
|
193
|
+
)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Search the GeoIP database for the specified host, returning city info.
|
197
|
+
#
|
198
|
+
# +hostname+ is a String holding the host's DNS name or numeric IP
|
199
|
+
# address.
|
200
|
+
#
|
201
|
+
# Returns a City object with the fourteen elements:
|
202
|
+
# * The host or IP address string as requested
|
203
|
+
# * The IP address string after looking up the host
|
204
|
+
# * The two-character country code (ISO 3166-1 alpha-2)
|
205
|
+
# * The three-character country code (ISO 3166-2 alpha-3)
|
206
|
+
# * The ISO 3166 English-language name of the country
|
207
|
+
# * The two-character continent code
|
208
|
+
# * The region name (state or territory)
|
209
|
+
# * The city name
|
210
|
+
# * The postal code (zipcode)
|
211
|
+
# * The latitude
|
212
|
+
# * The longitude
|
213
|
+
# * The USA dma_code if known (only REV1 City database)
|
214
|
+
# * The USA area_code if known (only REV1 City database)
|
215
|
+
# * The timezone name, if known
|
216
|
+
#
|
217
|
+
def city(hostname)
|
218
|
+
ip = lookup_ip(hostname)
|
219
|
+
|
220
|
+
# Convert numeric IP address to an integer
|
221
|
+
ipnum = iptonum(ip)
|
222
|
+
|
223
|
+
if @database_type != GEOIP_CITY_EDITION_REV0 &&
|
224
|
+
@database_type != GEOIP_CITY_EDITION_REV1
|
225
|
+
throw "Invalid GeoIP database type, can't look up City by IP"
|
226
|
+
end
|
227
|
+
|
228
|
+
pos = seek_record(ipnum)
|
229
|
+
|
230
|
+
# This next statement was added to MaxMind's C version after it was
|
231
|
+
# rewritten in Ruby. It prevents unassigned IP addresses from returning
|
232
|
+
# bogus data. There was concern over whether the changes to an
|
233
|
+
# application's behaviour were always correct, but this has been tested
|
234
|
+
# using an exhaustive search of the top 16 bits of the IP address space.
|
235
|
+
# The records where the change takes effect contained *no* valid data.
|
236
|
+
# If you're concerned, email me, and I'll send you the test program so
|
237
|
+
# you can test whatever IP range you think is causing problems,
|
238
|
+
# as I don't care to undertake an exhaustive search of the 32-bit space.
|
239
|
+
if pos != @database_segments[0]
|
240
|
+
read_city(pos, hostname, ip)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Search a ISP GeoIP database for the specified host, returning the ISP
|
245
|
+
# Not all GeoIP databases contain ISP information.
|
246
|
+
# Check http://maxmind.com
|
247
|
+
#
|
248
|
+
# +hostname+ is a String holding the host's DNS name or numeric IP
|
249
|
+
# address.
|
250
|
+
#
|
251
|
+
# Returns the ISP name.
|
252
|
+
#
|
253
|
+
def isp(hostname)
|
254
|
+
ip = lookup_ip(hostname)
|
255
|
+
|
256
|
+
# Convert numeric IP address to an integer
|
257
|
+
ipnum = iptonum(ip)
|
258
|
+
|
259
|
+
if @database_type != GEOIP_ISP_EDITION &&
|
260
|
+
@database_type != GEOIP_ORG_EDITION
|
261
|
+
throw "Invalid GeoIP database type, can't look up Organization/ISP by IP"
|
262
|
+
end
|
263
|
+
|
264
|
+
pos = seek_record(ipnum)
|
265
|
+
off = pos + (2 * @record_length - 1) * @database_segments[0]
|
266
|
+
|
267
|
+
record = atomic_read(MAX_ORG_RECORD_LENGTH, off)
|
268
|
+
record = record.sub(/\000.*/n, '')
|
269
|
+
record
|
270
|
+
end
|
271
|
+
|
272
|
+
# Search a ASN GeoIP database for the specified host, returning the AS
|
273
|
+
# number and description.
|
274
|
+
#
|
275
|
+
# +hostname+ is a String holding the host's DNS name or numeric
|
276
|
+
# IP address.
|
277
|
+
#
|
278
|
+
# Returns the AS number and description.
|
279
|
+
#
|
280
|
+
# Source:
|
281
|
+
# http://geolite.maxmind.com/download/geoip/database/asnum/GeoIPASNum.dat.gz
|
282
|
+
#
|
283
|
+
def asn(hostname)
|
284
|
+
ip = lookup_ip(hostname)
|
285
|
+
|
286
|
+
# Convert numeric IP address to an integer
|
287
|
+
ipnum = iptonum(ip)
|
288
|
+
|
289
|
+
if (@database_type != GEOIP_ASNUM_EDITION)
|
290
|
+
throw "Invalid GeoIP database type, can't look up ASN by IP"
|
291
|
+
end
|
292
|
+
|
293
|
+
pos = seek_record(ipnum)
|
294
|
+
off = pos + (2 * @record_length - 1) * @database_segments[0]
|
295
|
+
|
296
|
+
record = atomic_read(MAX_ASN_RECORD_LENGTH, off)
|
297
|
+
record = record.sub(/\000.*/n, '')
|
298
|
+
|
299
|
+
# AS####, Description
|
300
|
+
ASN.new($1, $2) if record =~ /^(AS\d+)\s(.*)$/
|
301
|
+
end
|
302
|
+
|
303
|
+
# Search a ISP GeoIP database for the specified host, returning the
|
304
|
+
# organization.
|
305
|
+
#
|
306
|
+
# +hostname+ is a String holding the host's DNS name or numeric
|
307
|
+
# IP address.
|
308
|
+
#
|
309
|
+
# Returns the organization associated with it.
|
310
|
+
#
|
311
|
+
alias_method(:organization, :isp) # Untested, according to Maxmind docs this should work
|
312
|
+
|
313
|
+
# Iterate through a GeoIP city database
|
314
|
+
def each
|
315
|
+
return enum_for unless block_given?
|
316
|
+
|
317
|
+
if @database_type != GEOIP_CITY_EDITION_REV0 &&
|
318
|
+
@database_type != GEOIP_CITY_EDITION_REV1
|
319
|
+
throw "Invalid GeoIP database type, can't iterate thru non-City database"
|
320
|
+
end
|
321
|
+
|
322
|
+
@iter_pos = @database_segments[0] + 1
|
323
|
+
num = 0
|
324
|
+
|
325
|
+
until (rec = read_city(@iter_pos)).nil?
|
326
|
+
yield rec
|
327
|
+
print "#{num}: #{@iter_pos}\n" if ((num += 1) % 1000) == 0
|
328
|
+
end
|
329
|
+
|
330
|
+
@iter_pos = nil
|
331
|
+
self
|
332
|
+
end
|
333
|
+
|
334
|
+
private
|
335
|
+
|
336
|
+
# Detects the type of the database.
|
337
|
+
def detect_database_type! # :nodoc:
|
338
|
+
@file.seek(-3, IO::SEEK_END)
|
339
|
+
|
340
|
+
0.upto(STRUCTURE_INFO_MAX_SIZE - 1) do |i|
|
341
|
+
if @file.read(3).bytes.all? { |byte| byte == 255 }
|
342
|
+
@database_type = if @file.respond_to?(:getbyte)
|
343
|
+
@file.getbyte
|
344
|
+
else
|
345
|
+
@file.getc
|
346
|
+
end
|
347
|
+
|
348
|
+
@database_type -= 105 if @database_type >= 106
|
349
|
+
|
350
|
+
if @database_type == GEOIP_REGION_EDITION_REV0
|
351
|
+
# Region Edition, pre June 2003
|
352
|
+
@database_segments = [STATE_BEGIN_REV0]
|
353
|
+
elsif @database_type == GEOIP_REGION_EDITION_REV1
|
354
|
+
# Region Edition, post June 2003
|
355
|
+
@database_segments = [STATE_BEGIN_REV1]
|
356
|
+
elsif @database_type == GEOIP_CITY_EDITION_REV0 ||
|
357
|
+
@database_type == GEOIP_CITY_EDITION_REV1 ||
|
358
|
+
@database_type == GEOIP_ORG_EDITION ||
|
359
|
+
@database_type == GEOIP_ISP_EDITION ||
|
360
|
+
@database_type == GEOIP_ASNUM_EDITION
|
361
|
+
|
362
|
+
# City/Org Editions have two segments, read offset of second segment
|
363
|
+
@database_segments = [0]
|
364
|
+
sr = @file.read(3).unpack('C*')
|
365
|
+
@database_segments[0] += le_to_ui(sr)
|
366
|
+
|
367
|
+
if @database_type == GEOIP_ORG_EDITION ||
|
368
|
+
@database_type == GEOIP_ISP_EDITION
|
369
|
+
@record_length = 4
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
break
|
374
|
+
else
|
375
|
+
@file.seek(-4, IO::SEEK_CUR)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
if @database_type == GEOIP_COUNTRY_EDITION ||
|
380
|
+
@database_type == GEOIP_PROXY_EDITION ||
|
381
|
+
@database_type == GEOIP_NETSPEED_EDITION
|
382
|
+
@database_segments = [COUNTRY_BEGIN]
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
# Search the GeoIP database for the specified host, returning city info.
|
387
|
+
#
|
388
|
+
# +hostname+ is a String holding the host's DNS name or numeric
|
389
|
+
# IP address.
|
390
|
+
#
|
391
|
+
# Returns an array of fourteen elements:
|
392
|
+
# * All elements from the country query (except GeoIP's country code,
|
393
|
+
# bah!)
|
394
|
+
# * The region (state or territory) name
|
395
|
+
# * The city name
|
396
|
+
# * The postal code (zipcode)
|
397
|
+
# * The latitude
|
398
|
+
# * The longitude
|
399
|
+
# * The dma_code and area_code, if available (REV1 City database)
|
400
|
+
# * The timezone name, if known
|
401
|
+
#
|
402
|
+
def read_city(pos, hostname = '', ip = '') #:nodoc:
|
403
|
+
off = pos + (2 * @record_length - 1) * @database_segments[0]
|
404
|
+
record = atomic_read(FULL_RECORD_LENGTH, off)
|
405
|
+
|
406
|
+
return unless record && record.size == FULL_RECORD_LENGTH
|
407
|
+
|
408
|
+
# The country code is the first byte:
|
409
|
+
code = record[0]
|
410
|
+
code = code.ord if code.respond_to?(:ord)
|
411
|
+
record = record[1..-1]
|
412
|
+
@iter_pos += 1 unless @iter_pos.nil?
|
413
|
+
|
414
|
+
spl = record.split("\x00", 4)
|
415
|
+
# Get the region:
|
416
|
+
region = spl[0]
|
417
|
+
@iter_pos += (region.size + 1) unless @iter_pos.nil?
|
418
|
+
|
419
|
+
# Get the city:
|
420
|
+
city = spl[1]
|
421
|
+
@iter_pos += (city.size + 1) unless @iter_pos.nil?
|
422
|
+
# set the correct encoding in ruby 1.9 compatible environments:
|
423
|
+
city.force_encoding('iso-8859-1') if city.respond_to?(:force_encoding)
|
424
|
+
|
425
|
+
# Get the postal code:
|
426
|
+
postal_code = spl[2]
|
427
|
+
@iter_pos += (postal_code.size + 1) unless @iter_pos.nil?
|
428
|
+
|
429
|
+
record = spl[3]
|
430
|
+
|
431
|
+
# Get the latitude/longitude:
|
432
|
+
if record && record[0, 3]
|
433
|
+
latitude = (le_to_ui(record[0, 3].unpack('C*')) / 10000.0) - 180
|
434
|
+
record = record[3..-1]
|
435
|
+
|
436
|
+
@iter_pos += 3 unless @iter_pos.nil?
|
437
|
+
else
|
438
|
+
latitude = ''
|
439
|
+
end
|
440
|
+
|
441
|
+
if record && record[0, 3]
|
442
|
+
longitude = le_to_ui(record[0, 3].unpack('C*')) / 10000.0 - 180
|
443
|
+
record = record[3..-1]
|
444
|
+
|
445
|
+
@iter_pos += 3 unless @iter_pos.nil?
|
446
|
+
else
|
447
|
+
longitude = ''
|
448
|
+
end
|
449
|
+
|
450
|
+
# UNTESTED
|
451
|
+
if record &&
|
452
|
+
record[0,3] &&
|
453
|
+
@database_type == GEOIP_CITY_EDITION_REV1 &&
|
454
|
+
CountryCode[code] == "US"
|
455
|
+
|
456
|
+
dmaarea_combo = le_to_ui(record[0,3].unpack('C*'))
|
457
|
+
dma_code = (dmaarea_combo / 1000)
|
458
|
+
area_code = (dmaarea_combo % 1000)
|
459
|
+
|
460
|
+
@iter_pos += 3 unless @iter_pos.nil?
|
461
|
+
else
|
462
|
+
dma_code = nil
|
463
|
+
area_code = nil
|
464
|
+
end
|
465
|
+
|
466
|
+
City.new(
|
467
|
+
hostname, # Requested hostname
|
468
|
+
ip, # Ip address as dotted quad
|
469
|
+
CountryCode[code], # ISO3166-1 code
|
470
|
+
CountryCode3[code], # ISO3166-2 code
|
471
|
+
CountryName[code], # Country name, per IS03166
|
472
|
+
CountryContinent[code], # Continent code.
|
473
|
+
region, # Region name
|
474
|
+
city, # City name
|
475
|
+
postal_code, # Postal code
|
476
|
+
latitude,
|
477
|
+
longitude,
|
478
|
+
dma_code,
|
479
|
+
area_code,
|
480
|
+
(TimeZone["#{CountryCode[code]}#{region}"] ||
|
481
|
+
TimeZone["#{CountryCode[code]}"])
|
482
|
+
)
|
483
|
+
end
|
484
|
+
|
485
|
+
def lookup_ip(ip_or_hostname) # :nodoc:
|
486
|
+
unless ip_or_hostname.is_a?(String) && ip_or_hostname !~ /^[0-9.]+$/
|
487
|
+
return ip_or_hostname
|
488
|
+
end
|
489
|
+
|
490
|
+
# Lookup IP address, we were given a name
|
491
|
+
ip = IPSocket.getaddress(ip_or_hostname)
|
492
|
+
ip = '0.0.0.0' if ip == '::1'
|
493
|
+
ip
|
494
|
+
end
|
495
|
+
|
496
|
+
# Convert numeric IP address to Integer.
|
497
|
+
def iptonum(ip) #:nodoc:
|
498
|
+
if ip.is_a?(String) &&
|
499
|
+
ip =~ %r{/^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/}
|
500
|
+
ip = be_to_ui(Regexp.last_match.to_a.slice(1..4))
|
501
|
+
else
|
502
|
+
ip = ip.to_i
|
503
|
+
end
|
504
|
+
|
505
|
+
ip
|
506
|
+
end
|
507
|
+
|
508
|
+
def seek_record(ipnum) #:nodoc:
|
509
|
+
# Binary search in the file.
|
510
|
+
# Records are pairs of little-endian integers, each of @record_length.
|
511
|
+
offset = 0
|
512
|
+
mask = 0x80000000
|
513
|
+
|
514
|
+
31.downto(0) do |depth|
|
515
|
+
off = (@record_length * 2 * offset)
|
516
|
+
buf = atomic_read(@record_length * 2, off)
|
517
|
+
|
518
|
+
buf.slice!(0...@record_length) if ((ipnum & mask) != 0)
|
519
|
+
offset = le_to_ui(buf[0...@record_length].unpack('C*'))
|
520
|
+
|
521
|
+
if offset >= @database_segments[0]
|
522
|
+
return offset
|
523
|
+
end
|
524
|
+
|
525
|
+
mask >>= 1
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
# Convert a big-endian array of numeric bytes to unsigned int.
|
530
|
+
#
|
531
|
+
# Returns the unsigned Integer.
|
532
|
+
#
|
533
|
+
def be_to_ui(s) #:nodoc:
|
534
|
+
i = 0
|
535
|
+
s.each { |b| i = ((i << 8) | (b.to_i & 0x0ff)) }
|
536
|
+
i
|
537
|
+
end
|
538
|
+
|
539
|
+
# Same for little-endian
|
540
|
+
def le_to_ui(s) #:nodoc:
|
541
|
+
be_to_ui(s.reverse)
|
542
|
+
end
|
543
|
+
|
544
|
+
# reads +length+ bytes from +offset+ as atomically as possible
|
545
|
+
# if IO.pread is available, it'll use that (making it both multithread
|
546
|
+
# and multiprocess-safe). Otherwise we will use a mutex to synchronize
|
547
|
+
# access (only providing protection against multiple threads, but not
|
548
|
+
# file descriptors shared across multiple processes).
|
549
|
+
def atomic_read(length, offset) #:nodoc:
|
550
|
+
if @mutex
|
551
|
+
@mutex.synchronize do
|
552
|
+
@file.seek(offset)
|
553
|
+
@file.read(length)
|
554
|
+
end
|
555
|
+
else
|
556
|
+
IO.pread(@file.fileno, length, offset)
|
557
|
+
end
|
558
|
+
end
|
559
|
+
end
|