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 +7 -0
- data/Gemfile +5 -0
- data/README.md +87 -0
- data/lib/geoip.rb +929 -0
- data/lib/rails_client_timezone/active_support_ext.rb +163 -0
- data/lib/rails_client_timezone/filter.rb +60 -0
- data/lib/rails_client_timezone/setting.rb +34 -0
- data/lib/rails_client_timezone/version.rb +3 -0
- data/rails_client_timezone.gemspec +30 -0
- metadata +80 -0
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
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
|