ip_filter 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -0
  4. data/CHANGELOG +29 -0
  5. data/Gemfile.lock +117 -0
  6. data/LICENSE +20 -0
  7. data/README.rdoc +232 -0
  8. data/Rakefile +6 -0
  9. data/data/geoip/country_code.yml +255 -0
  10. data/data/geoip/country_code3.yml +255 -0
  11. data/data/geoip/country_continent.yml +255 -0
  12. data/data/geoip/country_name.yml +255 -0
  13. data/data/geoip/time_zone.yml +677 -0
  14. data/lib/geoip.rb +559 -0
  15. data/lib/ip_filter.rb +100 -0
  16. data/lib/ip_filter/cache.rb +30 -0
  17. data/lib/ip_filter/cache/dallistore.rb +39 -0
  18. data/lib/ip_filter/cache/redis.rb +26 -0
  19. data/lib/ip_filter/configuration.rb +47 -0
  20. data/lib/ip_filter/controller/geo_ip_lookup.rb +78 -0
  21. data/lib/ip_filter/lookups/base.rb +60 -0
  22. data/lib/ip_filter/lookups/geoip.rb +41 -0
  23. data/lib/ip_filter/providers/max_mind.rb +52 -0
  24. data/lib/ip_filter/providers/s3.rb +51 -0
  25. data/lib/ip_filter/railtie.rb +23 -0
  26. data/lib/ip_filter/request.rb +14 -0
  27. data/lib/ip_filter/results/base.rb +39 -0
  28. data/lib/ip_filter/results/geoip.rb +19 -0
  29. data/lib/ip_filter/version.rb +3 -0
  30. data/spec/cache/dallistore_spec.rb +16 -0
  31. data/spec/cache/redis_spec.rb +56 -0
  32. data/spec/controller/ip_controller_spec.rb +56 -0
  33. data/spec/fixtures/GeoIP.dat +0 -0
  34. data/spec/fixtures/LICENSE.txt +31 -0
  35. data/spec/fixtures/country.dat +0 -0
  36. data/spec/ip_filter_spec.rb +19 -0
  37. data/spec/providers/max_mind_spec.rb +11 -0
  38. data/spec/providers/s3_spec.rb +11 -0
  39. data/spec/spec_helper.rb +40 -0
  40. data/spec/support/enable_dallistore_cache.rb +15 -0
  41. data/spec/support/enable_redis_cache.rb +15 -0
  42. metadata +85 -0
@@ -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