astro-em-dns 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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