rails_client_timezone 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d22bda7d6d4adcb829c4fb4c8c51c1fa9865b502
4
+ data.tar.gz: d4b910baf286d428bd80b7d156d7844b2447547e
5
+ SHA512:
6
+ metadata.gz: 684681aa4a8f0fe1ed5ffcaa47f7e95465aaa87e7fd5bbd06a19e518e424c06d3af239e2ae6f02387b8c1e7f7a845cfca9e49adeec2da0238bce8eb802adf290
7
+ data.tar.gz: 2940eca67bc9ce7ad1671a51ff2b84feb1569e9b45bd92b7fefa5b30e244b3ddb24102d715c6bf2f3a5bc23ac0f114c1cda0e34db8c96908dcbf7a03515ebd58
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "http://rubygems.org"
2
+ source 'http://gems.github.com/'
3
+
4
+ # Specify your gem's dependencies in rails_client_timezone.gemspec
5
+ gemspec
data/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # rails_client_timezone
2
+ ======================
3
+
4
+ The purpose of this gem is to track client's time zone for each request in the best possible way and run rails controller code in that time zone. Time zone can be tracked based on the ip OR browser's time zone OR a smarter combination of both.
5
+
6
+ # Instructions to use
7
+
8
+ ## Installation -
9
+
10
+ ```
11
+ gem 'rails_client_timezone', '0.7.0', :git => 'https://github.com/udayakiran/rails_client_timezone'
12
+ ```
13
+
14
+ ## Usage -
15
+
16
+
17
+ 1) Create a file in your initializers and set the 'mode' to detect the time zone. This step is optional. Default value of mode is :smart .
18
+
19
+ ```
20
+ # Say in config/initializers/rails_client_tz_init.rb
21
+ RailsClientTimezone::Setting.mode = :ip #default value is :smart. Accepted values - :browser, :ip, :smart
22
+ ```
23
+
24
+ :ip - Time zone will be detected based on the request's ip address(using geoip gem's logic)
25
+
26
+ :browser - Time zone will be detected based on browser utc offsets.
27
+
28
+ :smart - Time zone will be detected by browser if offsets are set or it falls back to ip(:browser mode first, which falls back to :ip mode).
29
+
30
+ 2) Include the around filter in every controller that needs to run code in user's timezone. If you need it for all controllers obviously add it to the application controller.
31
+
32
+ ```
33
+ prepend_around_filter RailsClientTimezone::Filter #Rails 4.1.x or earlier (inlcuding Rails 2,3 and 4)
34
+
35
+ prepend_around_action RailsClientTimezone::Filter #Rails 4.2.x or later
36
+ ```
37
+
38
+ ### For :browser and :smart mode -
39
+ Follow the below 2 steps if you are using :browser or :smart mode:
40
+
41
+ a) Create a file in your initializers and set baseline year This step is optional. This means we are worried about time zone changes that occurred till this year. My suggestion is to keep this year same as either the year you are starting this project or the year when your rails version is released.
42
+ Use "current" as the value if you want to stay on the edge. But, note that rails and your browsers need to be supporting this as well.
43
+
44
+ ```
45
+ # Say in config/initializers/rails_client_tz_init.rb
46
+ RailsClientTimezone::Setting.baseline_year = 2014 #default value is 2011. Accepted values - any valid year or string - "current"
47
+ ```
48
+
49
+ b) Add the js code that sets the browser offsets in your js files. Code can be found in assets directory based on the js lib you use.
50
+ If you use jquery, copy the js code from 'assets/set_browser_offset_cokies_jquery.js' and paste it in your js file which is inlcuded in every page.
51
+ If you use prototypejs, copy the js code 'from assets/set_browser_offset_cokies_prototype.js' and paste it in your js file which is inlcuded in every page.
52
+
53
+ ### For :ip and :smart mode -
54
+ Follow the below step if you are using :ip or :smart mode:
55
+
56
+ a) By default Geoip City db file is available in data directory, to override that db file you can download it from Download geoip city database from <geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz> and place anywhere in your app.
57
+ Create a file in your initializers and set the geo ip city db file path. This step is optional.
58
+
59
+ ```
60
+ # Say in config/initializers/rails_client_tz_init.rb
61
+ RailsClientTimezone::Setting.geoip_data_path = <file_path>
62
+ ```
63
+
64
+ ## Saving time zone in the database -
65
+
66
+ If you like to save the last_known_timezone of any user in the database, it can be done by accessing the cookie ":last_known_tz" any where in your controller.
67
+ so, 'controller.cookies[:last_known_tz]' would give you the user's last know timezone name once 'RailsClientTimezone::Filter' is done with the determining of the time zone from offsets.
68
+
69
+ ## Practices in your code -
70
+
71
+ 1. Use Time.zone.* not Time.* :- Most of the scenarios we need to deal with times in the current time zone not in the system time zone on which app is running. So, we should use Time.zone.now, Time.zone.parse and time_obj.in_time_zone(Time.zone) when we are dealing with time information.
72
+
73
+ 2. Use Time.use_zone :- When we need to operate in other time zones than the current system, enlose that code in Time.use_zone block. This sets back the system time zone once the code completes execution or even when exception occurs. Otherwise we should always remember to set the system's time zone back to default.
74
+
75
+ # To do -
76
+
77
+ Add specs and tests.
78
+
79
+ # Contributors -
80
+
81
+ Udayakiran
82
+
83
+ Yamini Devarajan
84
+
85
+ # Contributing -
86
+
87
+ Please help with your contribution by filing any issues if found. Pull requests are welcomed :)
data/lib/geoip.rb ADDED
@@ -0,0 +1,929 @@
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 Lesser 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
+ # Lesser General Public License for more details.
19
+ #
20
+ # You should have received a copy of the GNU Lesser General Public
21
+ # License along with this library; if not, write to the Free Software
22
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23
+ # = SYNOPSIS
24
+ #
25
+ # require 'geoip'
26
+ # p GeoIP.new('/usr/share/GeoIP/GeoIP.dat').country("www.netscape.sk")
27
+ #
28
+ # = DESCRIPTION
29
+ #
30
+ # GeoIP searches a GeoIP database for a given host or IP address, and
31
+ # returns information about the country where the IP address is allocated.
32
+ #
33
+ # = PREREQUISITES
34
+ #
35
+ # You need at least the free GeoIP.dat, for which the last known download
36
+ # location is <http://www.maxmind.com/download/geoip/database/GeoIP.dat.gz>
37
+ # This API requires the file to be decompressed for searching. Other versions
38
+ # of this database are available for purchase which contain more detailed
39
+ # information, but this information is not returned by this implementation.
40
+ # See www.maxmind.com for more information.
41
+ #
42
+
43
+ require 'thread' # Needed for Mutex
44
+ require 'socket'
45
+ begin
46
+ require 'io/extra' # for IO.pread
47
+ rescue LoadError
48
+ # oh well, hope they're not forking after initializing
49
+ end
50
+ begin
51
+ require 'ipaddr' # Needed for IPv6 support
52
+ rescue LoadError
53
+ # Won't work for an IPv6 database
54
+ end
55
+
56
+ require 'yaml'
57
+
58
+ class GeoIP
59
+
60
+ # The GeoIP GEM version number
61
+ VERSION = "1.6.1"
62
+
63
+ # The +data/+ directory for geoip
64
+ DATA_DIR = File.expand_path(File.join(File.dirname(__FILE__),'..','data','geoip'))
65
+
66
+ # Ordered list of the ISO3166 2-character country codes, ordered by
67
+ # GeoIP ID
68
+ CountryCode = YAML.load_file(File.join(DATA_DIR,'country_code.yml'))
69
+
70
+ # Ordered list of the ISO3166 3-character country codes, ordered by
71
+ # GeoIP ID
72
+ CountryCode3 = YAML.load_file(File.join(DATA_DIR,'country_code3.yml'))
73
+
74
+ # Ordered list of the English names of the countries, ordered by GeoIP ID
75
+ CountryName = YAML.load_file(File.join(DATA_DIR,'country_name.yml'))
76
+
77
+ # Ordered list of the ISO3166 2-character continent code of the countries,
78
+ # ordered by GeoIP ID
79
+ CountryContinent = YAML.load_file(File.join(DATA_DIR,'country_continent.yml'))
80
+
81
+ # Load a hash of region names by region code
82
+ RegionName = YAML.load_file(File.join(DATA_DIR,'region.yml'))
83
+
84
+ # Hash of the timezone codes mapped to timezone name, per zoneinfo
85
+ TimeZone = YAML.load_file(File.join(DATA_DIR,'time_zone.yml'))
86
+
87
+ GEOIP_COUNTRY_EDITION = 1
88
+ GEOIP_CITY_EDITION_REV1 = 2
89
+ GEOIP_REGION_EDITION_REV1 = 3
90
+ GEOIP_ISP_EDITION = 4
91
+ GEOIP_ORG_EDITION = 5
92
+ GEOIP_CITY_EDITION_REV0 = 6
93
+ GEOIP_REGION_EDITION_REV0 = 7
94
+ GEOIP_PROXY_EDITION = 8
95
+ GEOIP_ASNUM_EDITION = 9
96
+ GEOIP_NETSPEED_EDITION = 10
97
+ GEOIP_COUNTRY_EDITION_V6 = 12
98
+ GEOIP_CITY_EDITION_REV1_V6 = 30
99
+ GEOIP_NETSPEED_EDITION_REV1 = 32
100
+
101
+ # Editions list updated from the C API, August 2014:
102
+ module Edition
103
+ COUNTRY = 1
104
+ REGION_REV0 = 7
105
+ CITY_REV0 = 6
106
+ ORG = 5
107
+ ISP = 4
108
+ CITY_REV1 = 2
109
+ REGION_REV1 = 3
110
+ PROXY = 8
111
+ ASNUM = 9
112
+ NETSPEED = 10
113
+ DOMAIN = 11
114
+ COUNTRY_V6 = 12
115
+ LOCATIONA = 13
116
+ ACCURACYRADIUS = 14
117
+ CITYCONFIDENCE = 15 # unsupported
118
+ CITYCONFIDENCEDIST = 16 # unsupported
119
+ LARGE_COUNTRY = 17
120
+ LARGE_COUNTRY_V6 = 18
121
+ CITYCONFIDENCEDIST_ISP_ORG = 19 # unused, but gaps are not allowed
122
+ CCM_COUNTRY = 20 # unused, but gaps are not allowed
123
+ ASNUM_V6 = 21
124
+ ISP_V6 = 22
125
+ ORG_V6 = 23
126
+ DOMAIN_V6 = 24
127
+ LOCATIONA_V6 = 25
128
+ REGISTRAR = 26
129
+ REGISTRAR_V6 = 27
130
+ USERTYPE = 28
131
+ USERTYPE_V6 = 29
132
+ CITY_REV1_V6 = 30
133
+ CITY_REV0_V6 = 31
134
+ NETSPEED_REV1 = 32
135
+ NETSPEED_REV1_V6 = 33
136
+ COUNTRYCONF = 34
137
+ CITYCONF = 35
138
+ REGIONCONF = 36
139
+ POSTALCONF = 37
140
+ ACCURACYRADIUS_V6 = 38
141
+ end
142
+
143
+ # Numeric codes for NETSPEED (NETSPEED_REV1* is string-based):
144
+ GEOIP_UNKNOWN_SPEED = 0
145
+ GEOIP_DIALUP_SPEED = 1
146
+ GEOIP_CABLEDSL_SPEED = 2
147
+ GEOIP_CORPORATE_SPEED = 3
148
+
149
+ COUNTRY_BEGIN = 16776960 #:nodoc:
150
+ STATE_BEGIN_REV0 = 16700000 #:nodoc:
151
+ STATE_BEGIN_REV1 = 16000000 #:nodoc:
152
+ STRUCTURE_INFO_MAX_SIZE = 20 #:nodoc:
153
+ DATABASE_INFO_MAX_SIZE = 100 #:nodoc:
154
+ MAX_ORG_RECORD_LENGTH = 300 #:nodoc:
155
+ MAX_ASN_RECORD_LENGTH = 300 #:nodoc: unverified
156
+ US_OFFSET = 1 #:nodoc:
157
+ CANADA_OFFSET = 677 #:nodoc:
158
+ WORLD_OFFSET = 1353 #:nodoc:
159
+ FIPS_RANGE = 360 #:nodoc:
160
+ FULL_RECORD_LENGTH = 50 #:nodoc:
161
+
162
+ STANDARD_RECORD_LENGTH = 3 #:nodoc:
163
+ SEGMENT_RECORD_LENGTH = 3 #:nodoc:
164
+
165
+ class Country < Struct.new(:request, :ip, :country_code, :country_code2, :country_code3, :country_name, :continent_code)
166
+
167
+ def to_hash
168
+ Hash[each_pair.to_a]
169
+ end
170
+
171
+ end
172
+
173
+ class Region < Struct.new(:request, :ip, :country_code2, :country_code3, :country_name, :continent_code,
174
+ :region_code, :region_name, :timezone)
175
+
176
+ def to_hash
177
+ Hash[each_pair.to_a]
178
+ end
179
+
180
+ end
181
+
182
+ # Warning: for historical reasons the region code is mis-named region_name here
183
+ class City < Struct.new(:request, :ip, :country_code2, :country_code3, :country_name, :continent_code,
184
+ :region_name, :city_name, :postal_code, :latitude, :longitude, :dma_code, :area_code, :timezone, :real_region_name)
185
+
186
+ def to_hash
187
+ Hash[each_pair.to_a]
188
+ end
189
+
190
+ def region_code
191
+ self.region_name
192
+ end
193
+
194
+ end
195
+
196
+ class ASN < Struct.new(:number, :asn)
197
+
198
+ alias as_num number
199
+
200
+ def to_hash
201
+ Hash[each_pair.to_a]
202
+ end
203
+
204
+ end
205
+
206
+ class ISP < Struct.new(:isp)
207
+ def to_hash
208
+ Hash[each_pair.to_a]
209
+ end
210
+ end
211
+
212
+ # The Edition number that identifies which kind of database you've opened
213
+ attr_reader :database_type
214
+
215
+ # An IP that is used instead of local IPs
216
+ attr_accessor :local_ip_alias
217
+
218
+ alias databaseType database_type
219
+
220
+ # Open the GeoIP database and determine the file format version.
221
+ #
222
+ # +filename+ is a String holding the path to the GeoIP.dat file
223
+ # +options+ is a Hash allowing you to specify the caching options
224
+ #
225
+ def initialize(filename, options = {})
226
+ if options[:preload] || !IO.respond_to?(:pread)
227
+ @mutex = Mutex.new
228
+ end
229
+
230
+ @use_pread = IO.respond_to?(:pread) && !options[:preload]
231
+
232
+ @options = options
233
+ @database_type = Edition::COUNTRY
234
+ @record_length = STANDARD_RECORD_LENGTH
235
+ @file = File.open(filename, 'rb')
236
+
237
+ detect_database_type!
238
+
239
+ preload_data if options[:preload]
240
+ end
241
+
242
+ # Search the GeoIP database for the specified host, returning country
243
+ # info.
244
+ #
245
+ # +hostname+ is a String holding the host's DNS name or numeric IP
246
+ # address.
247
+ #
248
+ # If the database is a City database (normal), return the result that
249
+ # +city+ would return.
250
+ #
251
+ # Otherwise, return a Country object with the seven elements:
252
+ # * The host or IP address string as requested
253
+ # * The IP address string after looking up the host
254
+ # * The GeoIP country-ID as an integer (N.B. this is excluded from the
255
+ # city results!)
256
+ # * The two-character country code (ISO 3166-1 alpha-2)
257
+ # * The three-character country code (ISO 3166-2 alpha-3)
258
+ # * The ISO 3166 English-language name of the country
259
+ # * The two-character continent code
260
+ #
261
+ def country(hostname)
262
+ case @database_type
263
+ when Edition::CITY_REV0, Edition::CITY_REV1, Edition::CITY_REV1_V6
264
+ city(hostname)
265
+
266
+ when Edition::REGION_REV0, Edition::REGION_REV1
267
+ region(hostname)
268
+
269
+ when Edition::NETSPEED, Edition::NETSPEED_REV1
270
+ netspeed(hostname)
271
+
272
+ when Edition::COUNTRY, Edition::PROXY, Edition::COUNTRY_V6
273
+ ip = lookup_ip(hostname)
274
+ if @ip_bits > 32
275
+ ipaddr = IPAddr.new ip
276
+ code = (seek_record(ipaddr.to_i) - COUNTRY_BEGIN)
277
+ else
278
+ # Convert numeric IP address to an integer
279
+ ipnum = iptonum(ip)
280
+ code = (seek_record(ipnum) - @database_segments[0])
281
+ end
282
+ read_country(code, hostname, ip)
283
+ else
284
+ throw "Invalid GeoIP database type #{@database_type}, can't look up Country by IP"
285
+ end
286
+ end
287
+
288
+ # Search a GeoIP Connection Type (Netspeed) database for the specified host,
289
+ # returning the speed code.
290
+ #
291
+ # +hostname+ is a String holding the host's DNS name or numeric IP address.
292
+ def netspeed(hostname)
293
+ unless (@database_type == Edition::NETSPEED ||
294
+ @database_type == Edition::NETSPEED_REV1)
295
+ throw "Invalid GeoIP database type #{@database_type}, can't look up Netspeed by IP"
296
+ end
297
+ # Convert numeric IP address to an integer
298
+ ip = lookup_ip(hostname)
299
+ ipnum = iptonum(ip)
300
+ pos = seek_record(ipnum)
301
+ read_netspeed(pos-@database_segments[0])
302
+ end
303
+
304
+ # Search the GeoIP database for the specified host, returning region info.
305
+ #
306
+ # +hostname+ is a String holding the hosts's DNS name or numeric IP
307
+ # address.
308
+ #
309
+ # Returns a Region object with the nine elements:
310
+ # * The host or IP address string as requested
311
+ # * The IP address string after looking up the host
312
+ # * The two-character country code (ISO 3166-1 alpha-2)
313
+ # * The three-character country code (ISO 3166-2 alpha-3)
314
+ # * The ISO 3166 English-language name of the country
315
+ # * The two-character continent code
316
+ # * The region name (state or territory)
317
+ # * The timezone name, if known
318
+ #
319
+ def region(hostname)
320
+ if (@database_type == Edition::CITY_REV0 ||
321
+ @database_type == Edition::CITY_REV1 ||
322
+ @database_type == Edition::CITY_REV1_V6)
323
+ return city(hostname)
324
+ end
325
+
326
+ if (@database_type == Edition::REGION_REV0 ||
327
+ @database_type == Edition::REGION_REV1)
328
+ ip = lookup_ip(hostname)
329
+ ipnum = iptonum(ip)
330
+ pos = seek_record(ipnum)
331
+ else
332
+ throw "Invalid GeoIP database type, can't look up Region by IP"
333
+ end
334
+
335
+ if pos == @database_segments[0]
336
+ nil
337
+ else
338
+ read_region(pos, hostname, ip)
339
+ end
340
+ end
341
+
342
+ # Search the GeoIP database for the specified host, returning city info.
343
+ #
344
+ # +hostname+ is a String holding the host's DNS name or numeric IP
345
+ # address.
346
+ #
347
+ # Returns a City object with the fourteen elements:
348
+ # * The host or IP address string as requested
349
+ # * The IP address string after looking up the host
350
+ # * The two-character country code (ISO 3166-1 alpha-2)
351
+ # * The three-character country code (ISO 3166-2 alpha-3)
352
+ # * The ISO 3166 English-language name of the country
353
+ # * The two-character continent code
354
+ # * The region name (state or territory)
355
+ # * The city name
356
+ # * The postal code (zipcode)
357
+ # * The latitude
358
+ # * The longitude
359
+ # * The USA dma_code if known (only REV1 City database)
360
+ # * The USA area_code if known (only REV1 City database)
361
+ # * The timezone name, if known
362
+ #
363
+ def city(hostname)
364
+ ip = lookup_ip(hostname)
365
+
366
+ if (@database_type == Edition::CITY_REV0 ||
367
+ @database_type == Edition::CITY_REV1)
368
+ # Convert numeric IP address to an integer
369
+ ipnum = iptonum(ip)
370
+ pos = seek_record(ipnum)
371
+ elsif (@database_type == Edition::CITY_REV1_V6)
372
+ ipaddr = IPAddr.new ip
373
+ pos = seek_record(ipaddr.to_i)
374
+ else
375
+ throw "Invalid GeoIP database type, can't look up City by IP"
376
+ end
377
+
378
+ read_city(pos-@database_segments[0], hostname, ip)
379
+ end
380
+
381
+ # Search a ISP GeoIP database for the specified host, returning the ISP
382
+ # Not all GeoIP databases contain ISP information.
383
+ # Check http://maxmind.com
384
+ #
385
+ # +hostname+ is a String holding the host's DNS name or numeric IP
386
+ # address.
387
+ #
388
+ # Returns the ISP name.
389
+ #
390
+ def isp(hostname)
391
+ ip = lookup_ip(hostname)
392
+
393
+ # Convert numeric IP address to an integer
394
+ ipnum = iptonum(ip)
395
+
396
+ case @database_type
397
+ when Edition::ORG,
398
+ Edition::ISP,
399
+ Edition::DOMAIN,
400
+ Edition::ASNUM,
401
+ Edition::ACCURACYRADIUS,
402
+ Edition::NETSPEED,
403
+ Edition::USERTYPE,
404
+ Edition::REGISTRAR,
405
+ Edition::LOCATIONA,
406
+ Edition::CITYCONF,
407
+ Edition::COUNTRYCONF,
408
+ Edition::REGIONCONF,
409
+ Edition::POSTALCONF
410
+ pos = seek_record(ipnum)
411
+ read_isp(pos-@database_segments[0])
412
+ else
413
+ throw "Invalid GeoIP database type, can't look up Organization/ISP by IP"
414
+ end
415
+ end
416
+
417
+ # Search a ASN GeoIP database for the specified host, returning the AS
418
+ # number and description.
419
+ #
420
+ # Many other types of GeoIP database (e.g. userType) mis-identify as ASN type,
421
+ # and this can read those too.
422
+ #
423
+ # +hostname+ is a String holding the host's DNS name or numeric IP address.
424
+ #
425
+ # Returns the AS number and description.
426
+ #
427
+ # Source:
428
+ # http://geolite.maxmind.com/download/geoip/database/asnum/GeoIPASNum.dat.gz
429
+ #
430
+ def asn(hostname)
431
+ ip = lookup_ip(hostname)
432
+
433
+ # Convert numeric IP address to an integer
434
+ ipnum = iptonum(ip)
435
+
436
+ if ![Edition::ASNUM, Edition::ASNUM_V6].include? @database_type
437
+ throw "Invalid GeoIP database type #{@database_type}, can't look up ASN by IP"
438
+ end
439
+
440
+ pos = seek_record(ipnum)
441
+ read_asn(pos-@database_segments[0])
442
+ end
443
+
444
+ # Search a ISP GeoIP database for the specified host, returning the
445
+ # organization.
446
+ #
447
+ # +hostname+ is a String holding the host's DNS name or numeric
448
+ # IP address.
449
+ #
450
+ # Returns the organization associated with it.
451
+ #
452
+ alias_method(:organization, :isp) # Untested, according to Maxmind docs this should work
453
+
454
+ # Iterate through a GeoIP city database by
455
+ def each
456
+ return enum_for unless block_given?
457
+
458
+ if (@database_type != Edition::CITY_REV0 &&
459
+ @database_type != Edition::CITY_REV1)
460
+ throw "Invalid GeoIP database type, can't iterate thru non-City database"
461
+ end
462
+
463
+ @iter_pos = @database_segments[0] + 1
464
+ num = 0
465
+
466
+ until ((rec = read_city(@iter_pos)).nil?)
467
+ yield rec
468
+ print "#{num}: #{@iter_pos}\n" if((num += 1) % 1000 == 0)
469
+ end
470
+
471
+ @iter_pos = nil
472
+ return self
473
+ end
474
+
475
+ # Call like this, for example:
476
+ # GeoIP.new('GeoIPNetSpeedCell.dat').each{|*a| puts("0x%08X\t%d" % a)}
477
+ # or:
478
+ # GeoIP.new('GeoIPv6.dat').each{|*a| puts("0x%032X\t%d" % a)}
479
+ def each_by_ip offset = 0, ipnum = 0, mask = nil, &callback
480
+ mask ||= 1 << (@ip_bits-1)
481
+
482
+ # Read the two pointers and split them:
483
+ record2 = atomic_read(@record_length*2, @record_length*2*offset)
484
+ record1 = record2.slice!(0, @record_length)
485
+
486
+ # Traverse the left tree
487
+ off1 = le_to_ui(record1.unpack('C*'))
488
+ val = off1 - @database_segments[0]
489
+ if val >= 0
490
+ yield(ipnum, val > 0 ? read_record(ipnum.to_s, ipnum, val) : nil)
491
+ elsif mask != 0
492
+ each_by_ip(off1, ipnum, mask >> 1, &callback)
493
+ end
494
+
495
+ # Traverse the right tree
496
+ off2 = le_to_ui(record2.unpack('C*'))
497
+ val = off2 - @database_segments[0]
498
+ if val >= 0
499
+ yield(ipnum|mask, val > 0 ? read_record(ipnum.to_s, ipnum, val) : nil)
500
+ elsif mask != 0
501
+ each_by_ip(off2, ipnum|mask, mask >> 1, &callback)
502
+ end
503
+ end
504
+
505
+ private
506
+
507
+ def read_record hostname, ip, offset
508
+ case @database_type
509
+ when Edition::CITY_REV0, Edition::CITY_REV1, Edition::CITY_REV1_V6
510
+ read_city(offset, hostname, ip)
511
+
512
+ when Edition::REGION_REV0, Edition::REGION_REV1
513
+ read_region(offset+@database_segments[0], hostname, ip)
514
+
515
+ when Edition::NETSPEED, Edition::NETSPEED_REV1
516
+ read_netspeed(offset)
517
+
518
+ when Edition::COUNTRY, Edition::PROXY, Edition::COUNTRY_V6
519
+ read_country(offset, hostname, ip)
520
+
521
+ when Edition::ASNUM, Edition::ASNUM_V6
522
+ read_asn(offset)
523
+
524
+ # Add new types here
525
+ when Edition::ISP, Edition::ORG
526
+ read_isp offset
527
+
528
+ else
529
+ #raise "Unsupported GeoIP database type #{@database_type}"
530
+ offset
531
+ end
532
+ end
533
+
534
+ # Loads data into a StringIO which is Copy-on-write friendly
535
+ def preload_data
536
+ @file.seek(0)
537
+ @contents = StringIO.new(@file.read)
538
+ @file.close
539
+ end
540
+
541
+ # Detects the type of the database.
542
+ def detect_database_type! # :nodoc:
543
+ @file.seek(-3, IO::SEEK_END)
544
+ @ip_bits = 32
545
+
546
+ 0.upto(STRUCTURE_INFO_MAX_SIZE - 1) do |i|
547
+ if @file.read(3).bytes.all? { |byte| byte == 255 }
548
+ @database_type =
549
+ if @file.respond_to?(:getbyte)
550
+ @file.getbyte
551
+ else
552
+ @file.getc
553
+ end
554
+
555
+ @database_type -= 105 if @database_type >= 106
556
+
557
+ if (@database_type == Edition::REGION_REV0)
558
+ # Region Edition, pre June 2003
559
+ @database_segments = [STATE_BEGIN_REV0]
560
+ elsif (@database_type == Edition::REGION_REV1)
561
+ # Region Edition, post June 2003
562
+ @database_segments = [STATE_BEGIN_REV1]
563
+ elsif @database_type == Edition::CITY_REV0 ||
564
+ @database_type == Edition::CITY_REV1 ||
565
+ @database_type == Edition::ORG ||
566
+ @database_type == Edition::ORG_V6 ||
567
+ @database_type == Edition::ISP ||
568
+ @database_type == Edition::ISP_V6 ||
569
+ @database_type == Edition::REGISTRAR ||
570
+ @database_type == Edition::REGISTRAR_V6 ||
571
+ @database_type == Edition::USERTYPE || # Many of these files mis-identify as ASNUM files
572
+ @database_type == Edition::USERTYPE_V6 ||
573
+ @database_type == Edition::DOMAIN ||
574
+ @database_type == Edition::DOMAIN_V6 ||
575
+ @database_type == Edition::ASNUM ||
576
+ @database_type == Edition::ASNUM_V6 ||
577
+ @database_type == Edition::NETSPEED_REV1 ||
578
+ @database_type == Edition::NETSPEED_REV1_V6 ||
579
+ @database_type == Edition::LOCATIONA ||
580
+ # @database_type == Edition::LOCATIONA_V6 ||
581
+ @database_type == Edition::ACCURACYRADIUS ||
582
+ @database_type == Edition::ACCURACYRADIUS_V6 ||
583
+ @database_type == Edition::CITYCONF ||
584
+ @database_type == Edition::COUNTRYCONF ||
585
+ @database_type == Edition::REGIONCONF ||
586
+ @database_type == Edition::POSTALCONF ||
587
+ @database_type == Edition::CITY_REV0_V6 ||
588
+ @database_type == Edition::CITY_REV1_V6
589
+
590
+ # City/Org Editions have two segments, read offset of second segment
591
+ @database_segments = [0]
592
+ sr = @file.read(3).unpack("C*")
593
+ @database_segments[0] += le_to_ui(sr)
594
+
595
+ end
596
+
597
+ case @database_type
598
+ when Edition::COUNTRY
599
+ when Edition::NETSPEED_REV1
600
+ when Edition::ASNUM
601
+ when Edition::CITY_REV0
602
+ when Edition::CITY_REV1
603
+ when Edition::REGION_REV0
604
+ when Edition::REGION_REV1
605
+ @ip_bits = 32
606
+ @record_length = 3
607
+
608
+ when Edition::ORG,
609
+ Edition::DOMAIN,
610
+ Edition::ISP
611
+ @ip_bits = 32
612
+ @record_length = 4
613
+
614
+ when Edition::ASNUM_V6,
615
+ Edition::CITY_REV0_V6,
616
+ Edition::CITY_REV1_V6,
617
+ Edition::NETSPEED_REV1_V6,
618
+ Edition::COUNTRY_V6,
619
+ Edition::PROXY
620
+ @ip_bits = 128
621
+ @record_length = 3
622
+
623
+ when Edition::ACCURACYRADIUS_V6,
624
+ Edition::DOMAIN_V6,
625
+ Edition::ISP_V6,
626
+ Edition::LARGE_COUNTRY_V6,
627
+ Edition::LOCATIONA_V6,
628
+ Edition::ORG_V6,
629
+ Edition::REGISTRAR_V6,
630
+ Edition::USERTYPE_V6
631
+ @ip_bits = 128
632
+ @record_length = 4
633
+
634
+ else
635
+ raise "unimplemented database type"
636
+ end
637
+
638
+ break
639
+ else
640
+ @file.seek(-4, IO::SEEK_CUR)
641
+ end
642
+ end
643
+
644
+ if (@database_type == Edition::COUNTRY ||
645
+ @database_type == Edition::PROXY ||
646
+ @database_type == Edition::COUNTRY_V6 ||
647
+ @database_type == Edition::NETSPEED)
648
+ @database_segments = [COUNTRY_BEGIN]
649
+ end
650
+
651
+ # puts "Detected IPv#{@ip_bits == 32 ? '4' : '6'} database_type #{@database_type} with #{@database_segments[0]} records of length #{@record_length} (data starts at #{@database_segments[0]*@record_length*2})"
652
+ end
653
+
654
+ def read_country code, hostname, ip
655
+ Country.new(
656
+ hostname, # Requested hostname
657
+ ip, # Ip address as dotted quad
658
+ code, # GeoIP's country code
659
+ CountryCode[code], # ISO3166-1 alpha-2 code
660
+ CountryCode3[code], # ISO3166-2 alpha-3 code
661
+ CountryName[code], # Country name, per ISO 3166
662
+ CountryContinent[code] # Continent code.
663
+ )
664
+ end
665
+
666
+ def read_region(pos, hostname = '', ip = '') #:nodoc:
667
+ if (@database_type == Edition::REGION_REV0)
668
+ pos -= STATE_BEGIN_REV0
669
+ if (pos >= 1000)
670
+ code = 225
671
+ region_code = ((pos - 1000) / 26 + 65).chr + ((pos - 1000) % 26 + 65).chr
672
+ else
673
+ code = pos
674
+ region_code = ''
675
+ end
676
+ elsif (@database_type == Edition::REGION_REV1)
677
+ pos -= STATE_BEGIN_REV1
678
+ if (pos < US_OFFSET)
679
+ code = 0
680
+ region_code = ''
681
+ elsif (pos < CANADA_OFFSET)
682
+ code = 225
683
+ region_code = ((pos - US_OFFSET) / 26 + 65).chr + ((pos - US_OFFSET) % 26 + 65).chr
684
+ elsif (pos < WORLD_OFFSET)
685
+ code = 38
686
+ region_code = ((pos - CANADA_OFFSET) / 26 + 65).chr + ((pos - CANADA_OFFSET) % 26 + 65).chr
687
+ else
688
+ code = (pos - WORLD_OFFSET) / FIPS_RANGE
689
+ region_code = ''
690
+ end
691
+ end
692
+
693
+ Region.new(
694
+ hostname,
695
+ ip,
696
+ CountryCode[code], # ISO3166-1 alpha-2 code
697
+ CountryCode3[code], # ISO3166-2 alpha-3 code
698
+ CountryName[code], # Country name, per ISO 3166
699
+ CountryContinent[code], # Continent code.
700
+ region_code, # Unfortunately this is called region_name in the City structure
701
+ lookup_region_name(CountryCode[code], region_code),
702
+ (TimeZone["#{CountryCode[code]}#{region_code}"] || TimeZone["#{CountryCode[code]}"])
703
+ )
704
+ end
705
+
706
+ def read_asn offset
707
+ return nil if offset == 0
708
+ record = atomic_read(MAX_ASN_RECORD_LENGTH, index_size+offset)
709
+ record.slice!(record.index("\0")..-1)
710
+
711
+ # AS####, Description
712
+ # REVISIT: Text is in Latin-1 (ISO8859-1)
713
+ if record =~ /^(AS\d+)(?:\s(.*))?$/
714
+ ASN.new($1, $2)
715
+ else
716
+ record
717
+ end
718
+ end
719
+
720
+ def read_netspeed(offset)
721
+ return offset if @database_type == Edition::NETSPEED # Numeric value
722
+ return nil if offset == 0
723
+
724
+ record = atomic_read(20, index_size+offset)
725
+ record.slice!(record.index("\0")..-1)
726
+ record
727
+ end
728
+
729
+ def read_isp offset
730
+ record = atomic_read(MAX_ORG_RECORD_LENGTH, index_size+offset)
731
+ record = record.sub(/\000.*/n, '')
732
+ record.start_with?('*') ? nil : ISP.new(record)
733
+ end
734
+
735
+ # Size of the database index (a binary tree of depth <= @ip_bits)
736
+ def index_size
737
+ 2 * @record_length * @database_segments[0]
738
+ end
739
+
740
+ def lookup_region_name(country_iso2, region_code)
741
+ country_regions = RegionName[country_iso2]
742
+ country_regions && country_regions[region_code]
743
+ end
744
+
745
+ # Search the GeoIP database for the specified host, returning city info.
746
+ #
747
+ # +hostname+ is a String holding the host's DNS name or numeric
748
+ # IP address.
749
+ #
750
+ # Returns an array of fourteen elements:
751
+ # * All elements from the country query (except GeoIP's country code,
752
+ # bah!)
753
+ # * The region (state or territory) name
754
+ # * The city name
755
+ # * The postal code (zipcode)
756
+ # * The latitude
757
+ # * The longitude
758
+ # * The dma_code and area_code, if available (REV1 City database)
759
+ # * The timezone name, if known
760
+ #
761
+ def read_city(offset, hostname = '', ip = '') #:nodoc:
762
+ return nil if offset == 0
763
+ record = atomic_read(FULL_RECORD_LENGTH, offset+index_size)
764
+ return unless (record && record.size == FULL_RECORD_LENGTH)
765
+
766
+ # The country code is the first byte:
767
+ code = record[0]
768
+ code = code.ord if code.respond_to?(:ord)
769
+ record = record[1..-1]
770
+ @iter_pos += 1 unless @iter_pos.nil?
771
+
772
+ spl = record.split("\x00", 4)
773
+ # Get the region code:
774
+ region_code = spl[0]
775
+ @iter_pos += (region_code.size + 1) unless @iter_pos.nil?
776
+
777
+ # Get the city:
778
+ city = spl[1]
779
+ @iter_pos += (city.size + 1) unless @iter_pos.nil?
780
+ # set the correct encoding in ruby 1.9 compatible environments:
781
+ city = city.force_encoding('iso-8859-1').encode('utf-8') if city.respond_to?(:force_encoding)
782
+
783
+ # Get the postal code:
784
+ postal_code = spl[2]
785
+ @iter_pos += (postal_code.size + 1) unless @iter_pos.nil? || postal_code.nil?
786
+
787
+ record = spl[3]
788
+
789
+ # Get the latitude/longitude:
790
+ if (record && record[0,3])
791
+ latitude = (le_to_ui(record[0,3].unpack('C*')) / 10000.0) - 180
792
+ record = record[3..-1]
793
+
794
+ @iter_pos += 3 unless @iter_pos.nil?
795
+ else
796
+ latitude = ''
797
+ end
798
+
799
+ if (record && record[0,3])
800
+ longitude = le_to_ui(record[0,3].unpack('C*')) / 10000.0 - 180
801
+ record = record[3..-1]
802
+
803
+ @iter_pos += 3 unless @iter_pos.nil?
804
+ else
805
+ longitude = ''
806
+ end
807
+
808
+ # UNTESTED
809
+ if (record &&
810
+ record[0,3] &&
811
+ @database_type == Edition::CITY_REV1 &&
812
+ CountryCode[code] == "US")
813
+
814
+ dmaarea_combo = le_to_ui(record[0,3].unpack('C*'))
815
+ dma_code = (dmaarea_combo / 1000)
816
+ area_code = (dmaarea_combo % 1000)
817
+
818
+ @iter_pos += 3 unless @iter_pos.nil?
819
+ else
820
+ dma_code, area_code = nil, nil
821
+ end
822
+
823
+ City.new(
824
+ hostname, # Requested hostname
825
+ ip, # Ip address as dotted quad
826
+ CountryCode[code], # ISO3166-1 code
827
+ CountryCode3[code], # ISO3166-2 code
828
+ CountryName[code], # Country name, per IS03166
829
+ CountryContinent[code], # Continent code.
830
+ region_code, # Region code (called region_name, unfortunately)
831
+ city, # City name
832
+ postal_code, # Postal code
833
+ latitude,
834
+ longitude,
835
+ dma_code,
836
+ area_code,
837
+ (TimeZone["#{CountryCode[code]}#{region_code}"] || TimeZone["#{CountryCode[code]}"]),
838
+ lookup_region_name(CountryCode[code], region_code) # Real region name
839
+ )
840
+ end
841
+
842
+ def lookup_ip(ip_or_hostname) # :nodoc:
843
+ if is_local?(ip_or_hostname) && @local_ip_alias
844
+ ip_or_hostname = @local_ip_alias
845
+ end
846
+
847
+ if !ip_or_hostname.kind_of?(String) or ip_or_hostname =~ /^[0-9.]+$/
848
+ return ip_or_hostname
849
+ end
850
+
851
+ # Lookup IP address, we were given a name or IPv6 address
852
+ ip = IPSocket.getaddress(ip_or_hostname)
853
+ ip = '0.0.0.0' if ip == '::1'
854
+ ip
855
+ end
856
+
857
+ def is_local?(ip_or_hostname) #:nodoc:
858
+ ["127.0.0.1", "localhost", "::1", "0000::1", "0:0:0:0:0:0:0:1"].include? ip_or_hostname
859
+ end
860
+
861
+ # Convert numeric IP address to Integer.
862
+ def iptonum(ip) #:nodoc:
863
+ if (ip.kind_of?(String) &&
864
+ ip =~ /^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/)
865
+ ip = be_to_ui(Regexp.last_match().to_a.slice(1..4))
866
+ else
867
+ ip = ip.to_i
868
+ end
869
+
870
+ return ip
871
+ end
872
+
873
+ def seek_record(ipnum) #:nodoc:
874
+ # Binary search in the file.
875
+ # Records are pairs of little-endian integers, each of @record_length.
876
+ offset = 0
877
+ mask = 1 << (@ip_bits-1)
878
+
879
+ @ip_bits.downto(1) do |depth|
880
+ go_right = (ipnum & mask) != 0
881
+ off = @record_length * (2 * offset + (go_right ? 1 : 0))
882
+ offset = le_to_ui(r2 = atomic_read(@record_length, off).unpack('C*'))
883
+
884
+ return offset if offset >= @database_segments[0]
885
+ mask >>= 1
886
+ end
887
+ end
888
+
889
+ # Convert a big-endian array of numeric bytes to unsigned int.
890
+ #
891
+ # Returns the unsigned Integer.
892
+ #
893
+ def be_to_ui(s) #:nodoc:
894
+ i = 0
895
+
896
+ s.each { |b| i = ((i << 8) | (b.to_i & 0x0ff)) }
897
+ return i
898
+ end
899
+
900
+ # Same for little-endian
901
+ def le_to_ui(s) #:nodoc:
902
+ be_to_ui(s.reverse)
903
+ end
904
+
905
+ # reads +length+ bytes from +pos+ as atomically as possible
906
+ # if IO.pread is available, it'll use that (making it both multithread
907
+ # and multiprocess-safe). Otherwise we'll use a mutex to synchronize
908
+ # access (only providing protection against multiple threads, but not
909
+ # file descriptors shared across multiple processes).
910
+ # If the contents of the database have been preloaded it'll work with
911
+ # the StringIO object directly.
912
+ def atomic_read(length, pos) #:nodoc:
913
+ if @mutex
914
+ @mutex.synchronize { atomic_read_unguarded(length, pos) }
915
+ else
916
+ atomic_read_unguarded(length, pos)
917
+ end
918
+ end
919
+
920
+ def atomic_read_unguarded(length, pos)
921
+ if @use_pread
922
+ IO.pread(@file.fileno, length, pos)
923
+ else
924
+ io = @contents || @file
925
+ io.seek(pos)
926
+ io.read(length)
927
+ end
928
+ end
929
+ end