astro-em-dns 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. data/Rakefile +21 -0
  2. data/lib/em/dns_cache.rb +294 -0
  3. data/test/test_basic.rb +222 -0
  4. metadata +65 -0
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ begin
2
+ require 'jeweler'
3
+ rescue LoadError
4
+ raise "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
5
+ end
6
+
7
+ Jeweler::Tasks.new do |s|
8
+ s.name = "em-dns"
9
+ s.summary = "Resolve domain names from EventMachine natively"
10
+ s.email = "astro@spaceboyz.net"
11
+ s.homepage = "http://github.com/astro/em-dns"
12
+ s.description = "DNS::Resolv made ready for EventMachine"
13
+ s.authors = ["Aman Gupta", "Stephan Maka"]
14
+ s.files = FileList["[A-Z]*", "{lib,test}/**/*"]
15
+ s.add_dependency 'eventmachine'
16
+ end
17
+
18
+ require 'rake/testtask'
19
+ Rake::TestTask.new do |t|
20
+ t.test_files = FileList["test/test*.rb"]
21
+ end
@@ -0,0 +1,294 @@
1
+ # $Id: dns_cache.rb 5040 2007-10-05 17:31:04Z francis $
2
+ #
3
+ #
4
+
5
+ require 'rubygems'
6
+ require 'eventmachine'
7
+ require 'resolv'
8
+
9
+
10
+ module EventMachine
11
+ module DnsCache
12
+
13
+ class Cache
14
+ def initialize
15
+ @hash = {}
16
+ end
17
+ def add domain, value, expiration
18
+ ex = ((expiration < 0) ? :none : (Time.now + expiration))
19
+ @hash[domain] = [ex, value]
20
+ end
21
+ def retrieve domain
22
+ if @hash.has_key?(domain)
23
+ d = @hash[domain]
24
+ if d.first != :none and d.first < Time.now
25
+ @hash.delete(domain)
26
+ nil
27
+ else
28
+ d.last
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ @a_cache = Cache.new
35
+ @mx_cache = Cache.new
36
+ @nameservers = []
37
+ @message_ix = 0
38
+
39
+ def self.add_nameserver ns
40
+ @nameservers << ns unless @nameservers.include?(ns)
41
+ end
42
+
43
+ def self.verbose v=true
44
+ @verbose = v
45
+ end
46
+
47
+
48
+ def self.add_cache_entry cache_type, domain, value, expiration
49
+ cache = if cache_type == :mx
50
+ @mx_cache
51
+ elsif cache_type == :a
52
+ @a_cache
53
+ else
54
+ raise "bad cache type"
55
+ end
56
+
57
+ v = EM::DefaultDeferrable.new
58
+ v.succeed( value.dup.freeze )
59
+ cache.add domain, v, expiration
60
+ end
61
+
62
+ # Needs to be DRYed up with resolve_mx.
63
+ #
64
+ def self.resolve domain
65
+ if d = @a_cache.retrieve(domain)
66
+ d
67
+ else
68
+ =begin
69
+ d = @a_cache[domain]
70
+ if d.first < Time.now
71
+ $>.puts "Expiring stale cache entry for #{domain}" if @verbose
72
+ @a_cache.delete domain
73
+ resolve domain
74
+ else
75
+ $>.puts "Fulfilled #{domain} from cache" if @verbose
76
+ d.last
77
+ end
78
+ else
79
+ =end
80
+ $>.puts "Fulfilling #{domain} from network" if @verbose
81
+ d = EM::DefaultDeferrable.new
82
+ d.timeout(5)
83
+ @a_cache.add domain, d, 300 # Hard-code a 5 minute expiration
84
+ #@a_cache[domain] = [Time.now+120, d] # Hard code a 120-second expiration.
85
+
86
+ lazy_initialize
87
+ m = Resolv::DNS::Message.new
88
+ m.rd = 1
89
+ m.add_question domain, Resolv::DNS::Resource::IN::A
90
+ m = m.encode
91
+ @nameservers.each {|ns|
92
+ @message_ix = (@message_ix + 1) % 60000
93
+ Request.new d, @message_ix
94
+ msg = m.dup
95
+ msg[0,2] = [@message_ix].pack("n")
96
+ @u.send_datagram msg, ns, 53
97
+ }
98
+
99
+ d.callback {|resp|
100
+ r = []
101
+ resp.each_answer {|name,ttl,data|
102
+ r << data.address.to_s if data.kind_of?(Resolv::DNS::Resource::IN::A)
103
+ }
104
+
105
+ # Freeze the array since we'll be keeping it in cache and passing it
106
+ # around to multiple users. And alternative would have been to dup it.
107
+ r.freeze
108
+ d.succeed r
109
+ }
110
+
111
+
112
+ d
113
+ end
114
+ end
115
+
116
+
117
+ # Needs to be DRYed up with resolve.
118
+ #
119
+ def self.resolve_mx domain
120
+ if d = @mx_cache.retrieve(domain)
121
+ d
122
+ else
123
+ =begin
124
+ if @mx_cache.has_key?(domain)
125
+ d = @mx_cache[domain]
126
+ if d.first < Time.now
127
+ $>.puts "Expiring stale cache entry for #{domain}" if @verbose
128
+ @mx_cache.delete domain
129
+ resolve_mx domain
130
+ else
131
+ $>.puts "Fulfilled #{domain} from cache" if @verbose
132
+ d.last
133
+ end
134
+ else
135
+ =end
136
+ $>.puts "Fulfilling #{domain} from network" if @verbose
137
+ d = EM::DefaultDeferrable.new
138
+ d.timeout(5)
139
+ #@mx_cache[domain] = [Time.now+120, d] # Hard code a 120-second expiration.
140
+ @mx_cache.add domain, d, 300 # Hard-code a 5 minute expiration
141
+
142
+ mx_query = MxQuery.new d
143
+
144
+ lazy_initialize
145
+ m = Resolv::DNS::Message.new
146
+ m.rd = 1
147
+ m.add_question domain, Resolv::DNS::Resource::IN::MX
148
+ m = m.encode
149
+ @nameservers.each {|ns|
150
+ @message_ix = (@message_ix + 1) % 60000
151
+ Request.new mx_query, @message_ix
152
+ msg = m.dup
153
+ msg[0,2] = [@message_ix].pack("n")
154
+ @u.send_datagram msg, ns, 53
155
+ }
156
+
157
+
158
+
159
+ d
160
+ end
161
+
162
+ end
163
+
164
+
165
+ def self.lazy_initialize
166
+ # Will throw an exception if EM is not running.
167
+ # We wire a signaller into the socket handler to tell us when that socket
168
+ # goes away. (Which can happen, among other things, if the reactor
169
+ # stops and restarts.)
170
+ #
171
+ raise "EventMachine reactor not running" unless EM.reactor_running?
172
+
173
+ unless @u
174
+ us = proc {@u = nil}
175
+ @u = EM::open_datagram_socket( "0.0.0.0", 0, Socket ) {|c|
176
+ c.unbind_signaller = us
177
+ }
178
+ end
179
+
180
+ end
181
+
182
+
183
+ def self.parse_local_mx_records txt
184
+ domain = nil
185
+ addrs = []
186
+
187
+ add_it = proc {
188
+ a = addrs.sort {|m,n| m.last <=> n.last}.map {|y| y.first}
189
+ add_cache_entry :mx, domain, a, -1
190
+ }
191
+
192
+ txt = StringIO.new( txt ) if txt.is_a?(String)
193
+ txt.each_line {|ln|
194
+ if ln =~ /\A\s*([\d\w\.\-\_]+)\s+(\d+)\s*\Z/
195
+ if domain
196
+ addrs << [$1.dup, $2.dup.to_i]
197
+ end
198
+ elsif ln =~ /\A\s*([^\s\:]+)\s*\:\s*\Z/
199
+ add_it.call if domain
200
+ domain = $1.dup
201
+ addrs.clear
202
+ end
203
+ }
204
+
205
+ add_it.call if domain
206
+ end
207
+
208
+
209
+ class MxQuery
210
+ include EM::Deferrable
211
+
212
+ def initialize rslt
213
+ @result = rslt # Deferrable
214
+ @n_a_lookups = 0
215
+
216
+ self.callback {|resp|
217
+ addrs = {}
218
+ resp.each_additional {|name,ttl,data|
219
+ addrs.has_key?(name) ? (addrs[name] << data.address.to_s) : (addrs[name] = [data.address.to_s])
220
+ }
221
+
222
+ @addresses = resp.answer.
223
+ sort {|a,b| a[2].preference <=> b[2].preference}.
224
+ map {|name,ttl,data|
225
+ ex = data.exchange
226
+ addrs[ex] or EM::DnsCache.resolve(ex.to_s)
227
+ }
228
+
229
+ @addresses.each_with_index {|a,ix|
230
+ if a.respond_to?(:set_deferred_status)
231
+ @n_a_lookups += 1
232
+ a.callback {|r|
233
+ @addresses[ix] = r
234
+ @n_a_lookups -= 1
235
+ succeed_result if @n_a_lookups == 0
236
+ }
237
+ end
238
+ }
239
+
240
+ succeed_result if @n_a_lookups == 0
241
+ }
242
+ end
243
+
244
+ def succeed_result
245
+ # Questionable whether we should uniq if it perturbs the sort order.
246
+ # Also freeze it so some user can't wipe it out on us.
247
+ @result.succeed @addresses.flatten.uniq.freeze
248
+ end
249
+
250
+ end
251
+
252
+ class Request
253
+ include EM::Deferrable
254
+
255
+ @@outstanding = {}
256
+
257
+ def self.post response
258
+ if r = @@outstanding.delete(response.id)
259
+ r.succeed response
260
+ end
261
+ end
262
+
263
+ def initialize rslt, m_id
264
+ @result = rslt
265
+ @msgid = m_id
266
+ raise "request-queue overflow" if @@outstanding.has_key?(@msgid)
267
+ @@outstanding[@msgid] = self
268
+
269
+ self.timeout(10)
270
+ self.errback { @@outstanding.delete(@msgid) }
271
+ self.callback {|resp| @result.succeed resp }
272
+ end
273
+ end
274
+
275
+ class Socket < EM::Connection
276
+ attr_accessor :unbind_signaller
277
+
278
+ def receive_data dg
279
+ m = nil
280
+ begin
281
+ m = Resolv::DNS::Message.decode dg
282
+ rescue
283
+ end
284
+ Request.post(m) if m
285
+ end
286
+
287
+ def unbind
288
+ @unbind_signaller.call if @unbind_signaller
289
+ end
290
+ end
291
+
292
+ end
293
+ end
294
+
@@ -0,0 +1,222 @@
1
+ # $Id: test_basic.rb 5040 2007-10-05 17:31:04Z francis $
2
+ #
3
+ #
4
+
5
+ require 'test/unit'
6
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
7
+ require 'em/dns_cache'
8
+
9
+
10
+
11
+
12
+ #--------------------------------------
13
+
14
+
15
+ class TestBasic < Test::Unit::TestCase
16
+
17
+ TestNameserver = "151.202.0.85"
18
+ TestNameserver2 = "151.202.0.86"
19
+
20
+ LocalMxRecords = %Q(
21
+ boondoggle.zzz:
22
+ 65.66.67.68 10
23
+ 55.56.57.58 5
24
+ esmtp.someone.zzz 4
25
+
26
+ boondoggle.yyy:
27
+ )
28
+
29
+ def test_a
30
+ EM::DnsCache.add_nameserver TestNameserver
31
+ EM::DnsCache.verbose
32
+
33
+ out = nil
34
+
35
+ EM.run {
36
+ d = EM::DnsCache.resolve "bayshorenetworks.com"
37
+ d.errback {EM.stop}
38
+ d.callback {|r|
39
+ d = EM::DnsCache.resolve "bayshorenetworks.com"
40
+ d.errback { EM.stop }
41
+ d.callback {|r| puts r; out = r; EM.stop }
42
+ }
43
+ }
44
+
45
+ assert out
46
+ end
47
+
48
+ def test_a_pair
49
+ EM::DnsCache.add_nameserver TestNameserver
50
+ EM::DnsCache.verbose
51
+
52
+ out = nil
53
+
54
+ EM.run {
55
+ d = EM::DnsCache.resolve "maila.microsoft.com"
56
+ d.errback {EM.stop}
57
+ d.callback {|r|
58
+ out = r
59
+ EM.stop
60
+ }
61
+ }
62
+
63
+ assert_equal( Array, out.class )
64
+ assert_equal( 2, out.length )
65
+ end
66
+
67
+
68
+ # This test causes each request to hit the network because they're all scheduled
69
+ # before the first one can come back and load the cache. Although a nice mis-feature for
70
+ # stress testing, it would be nice to fix it someday, perhaps by not kicking off a
71
+ # request for a particular domain if one is already pending.
72
+ # Without epoll, this test gets really slow and usually doesn't complete.
73
+ def test_lots_of_a
74
+ EM.epoll
75
+ EM::DnsCache.add_nameserver TestNameserver
76
+ EM::DnsCache.add_nameserver TestNameserver2
77
+ EM::DnsCache.verbose
78
+
79
+ n = 250
80
+ e = 0
81
+ s = 0
82
+ EM.run {
83
+ n.times {
84
+ d = EM::DnsCache.resolve "ibm.com"
85
+ d.errback {e+=1; n -= 1; EM.stop if n == 0}
86
+ d.callback {s+=1; n -= 1; EM.stop if n == 0}
87
+ }
88
+ }
89
+ assert_equal( 0, n)
90
+ assert_equal( 250, s)
91
+ end
92
+
93
+
94
+
95
+
96
+ def test_mx
97
+ EM::DnsCache.add_nameserver TestNameserver
98
+ EM::DnsCache.verbose
99
+
100
+ out = nil
101
+
102
+ EM.run {
103
+ d = EM::DnsCache.resolve_mx "steamheat.net"
104
+ d.errback {EM.stop}
105
+ d.callback {|r|
106
+ p r
107
+ d = EM::DnsCache.resolve_mx "steamheat.net"
108
+ d.errback {EM.stop}
109
+ d.callback {|r|
110
+ out = r
111
+ p r
112
+ EM.stop
113
+ }
114
+ }
115
+ }
116
+
117
+ assert out
118
+ end
119
+
120
+
121
+ # The arrays of addresses we get back from the DnsCache are FROZEN.
122
+ # That's because the same objects get passed around to any caller that
123
+ # asks for them. If you need to modify the array, dup it.
124
+ #
125
+ def test_freeze
126
+ EM::DnsCache.add_nameserver TestNameserver
127
+ EM::DnsCache.verbose
128
+
129
+ out = nil
130
+
131
+ EM.run {
132
+ d = EM::DnsCache.resolve_mx "steamheat.net"
133
+ d.errback {EM.stop}
134
+ d.callback {|r|
135
+ out = r
136
+ EM.stop
137
+ }
138
+ }
139
+
140
+ assert out
141
+ assert( out.length > 0)
142
+ assert_raise( TypeError ) {
143
+ out.clear
144
+ }
145
+ end
146
+
147
+
148
+ def test_local_defs
149
+ EM::DnsCache.add_nameserver TestNameserver
150
+ EM::DnsCache.verbose
151
+
152
+ EM::DnsCache.add_cache_entry( :mx, "example.zzz", ["1.2.3.4"], -1 )
153
+ out = nil
154
+ EM.run {
155
+ d = EM::DnsCache.resolve_mx "example.zzz"
156
+ d.errback {EM.stop}
157
+ d.callback {|r|
158
+ out = r
159
+ EM.stop
160
+ }
161
+ }
162
+ assert_equal( ["1.2.3.4"], out )
163
+ end
164
+
165
+ def test_multiple_local_defs
166
+ EM::DnsCache.verbose
167
+
168
+ EM::DnsCache.add_cache_entry( :mx, "example.zzz", ["1.2.3.4", "5.6.7.8"], -1 )
169
+ out = nil
170
+ EM.run {
171
+ d = EM::DnsCache.resolve_mx "example.zzz"
172
+ d.errback {EM.stop}
173
+ d.callback {|r|
174
+ out = r
175
+ EM.stop
176
+ }
177
+ }
178
+ assert_equal( ["1.2.3.4","5.6.7.8"], out )
179
+ end
180
+
181
+ # Adding cache entries where they already exist causes them to be REPLACED.
182
+ #
183
+ def test_replace
184
+ EM::DnsCache.verbose
185
+
186
+ EM::DnsCache.add_cache_entry( :mx, "example.zzz", ["1.2.3.4", "5.6.7.8"], -1 )
187
+ EM::DnsCache.add_cache_entry( :mx, "example.zzz", ["10.11.12.13"], -1 )
188
+ out = nil
189
+ EM.run {
190
+ d = EM::DnsCache.resolve_mx "example.zzz"
191
+ d.errback {EM.stop}
192
+ d.callback {|r|
193
+ out = r
194
+ EM.stop
195
+ }
196
+ }
197
+ assert_equal( ["10.11.12.13"], out )
198
+ end
199
+
200
+ # We have a facility for storing locally-defined MX records.
201
+ # The DNS cache has a way to parse and process these values.
202
+ #
203
+ def test_external_mx_defs
204
+ EM::DnsCache.verbose
205
+
206
+ EM::DnsCache.parse_local_mx_records LocalMxRecords
207
+
208
+ out = nil
209
+ EM.run {
210
+ d = EM::DnsCache.resolve_mx "boondoggle.zzz"
211
+ d.errback {EM.stop}
212
+ d.callback {|r|
213
+ out = r
214
+ EM.stop
215
+ }
216
+ }
217
+ assert_equal( ["esmtp.someone.zzz", "55.56.57.58", "65.66.67.68"], out )
218
+ end
219
+
220
+ end
221
+
222
+
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: astro-em-dns
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Aman Gupta
8
+ - Stephan Maka
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-05-17 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: eventmachine
18
+ type: :runtime
19
+ version_requirement:
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ version:
26
+ description: DNS::Resolv made ready for EventMachine
27
+ email: astro@spaceboyz.net
28
+ executables: []
29
+
30
+ extensions: []
31
+
32
+ extra_rdoc_files: []
33
+
34
+ files:
35
+ - Rakefile
36
+ - lib/em/dns_cache.rb
37
+ - test/test_basic.rb
38
+ has_rdoc: true
39
+ homepage: http://github.com/astro/em-dns
40
+ post_install_message:
41
+ rdoc_options:
42
+ - --charset=UTF-8
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ requirements: []
58
+
59
+ rubyforge_project:
60
+ rubygems_version: 1.2.0
61
+ signing_key:
62
+ specification_version: 3
63
+ summary: Resolve domain names from EventMachine natively
64
+ test_files:
65
+ - test/test_basic.rb