File: server.rb

package info (click to toggle)
ruby-rubydns 1.0.3-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye
  • size: 548 kB
  • sloc: ruby: 1,796; makefile: 7
file content (338 lines) | stat: -rw-r--r-- 10,812 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# Copyright, 2009, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

require 'celluloid/io'

require_relative 'transaction'
require_relative 'logger'

module RubyDNS
	class UDPSocketWrapper < Celluloid::IO::UDPSocket
		def initialize(socket)
			@socket = socket
		end
	end
	
	class TCPServerWrapper < Celluloid::IO::TCPServer
		def initialize(server)
			@server = server
		end
	end
	
	class Server
		include Celluloid::IO
		
		finalizer :shutdown
		
		# The default server interfaces
		DEFAULT_INTERFACES = [[:udp, "0.0.0.0", 53], [:tcp, "0.0.0.0", 53]]
		
		# Instantiate a server with a block
		#
		#	server = Server.new do
		#		match(/server.mydomain.com/, IN::A) do |transaction|
		#			transaction.respond!("1.2.3.4")
		#		end
		#	end
		#
		def initialize(options = {})
			@handlers = []
			
			@logger = options[:logger] || Celluloid.logger
			@interfaces = options[:listen] || DEFAULT_INTERFACES
			
			@origin = options[:origin] || '.'
		end

		# Records are relative to this origin:
		attr_accessor :origin

		attr_accessor :logger

		# Fire the named event as part of running the server.
		def fire(event_name)
		end
		
		def shutdown
			fire(:stop)
		end
		
		# Give a name and a record type, try to match a rule and use it for processing the given arguments.
		def process(name, resource_class, transaction)
			raise NotImplementedError.new
		end
		
		# Process an incoming DNS message. Returns a serialized message to be sent back to the client.
		def process_query(query, options = {}, &block)
			start_time = Time.now
			
			# Setup response
			response = Resolv::DNS::Message::new(query.id)
			response.qr = 1                 # 0 = Query, 1 = Response
			response.opcode = query.opcode  # Type of Query; copy from query
			response.aa = 1                 # Is this an authoritative response: 0 = No, 1 = Yes
			response.rd = query.rd          # Is Recursion Desired, copied from query
			response.ra = 0                 # Does name server support recursion: 0 = No, 1 = Yes
			response.rcode = 0              # Response code: 0 = No errors
			
			transaction = nil
			
			begin
				query.question.each do |question, resource_class|
					begin
						question = question.without_origin(@origin)
						
						@logger.debug {"<#{query.id}> Processing question #{question} #{resource_class}..."}
						
						transaction = Transaction.new(self, query, question, resource_class, response, options)
						
						transaction.process
					rescue Resolv::DNS::OriginError
						# This is triggered if the question is not part of the specified @origin:
						@logger.debug {"<#{query.id}> Skipping question #{question} #{resource_class} because #{$!}"}
					end
				end
			rescue Celluloid::ResumableError
				raise
			rescue StandardError => error
				@logger.error "<#{query.id}> Exception thrown while processing #{transaction}!"
				RubyDNS.log_exception(@logger, error)
			
				response.rcode = Resolv::DNS::RCode::ServFail
			end
			
			end_time = Time.now
			@logger.debug {"<#{query.id}> Time to process request: #{end_time - start_time}s"}
			
			return response
		end
		
		#
		# By default the server runs on port 53, both TCP and UDP, which is usually a priviledged port and requires root access to bind. You can change this by specifying `options[:listen]` which should contain an array of `[protocol, interface address, port]` specifications.
		# 
		#	INTERFACES = [[:udp, "0.0.0.0", 5300]]
		#	RubyDNS::run_server(:listen => INTERFACES) do
		#		...
		#	end
		#
		# You can specify already connected sockets if need be:
		#
		#   socket = UDPSocket.new; socket.bind("0.0.0.0", 53)
		#   Process::Sys.setuid(server_uid)
		#   INTERFACES = [socket]
		#
		def run
			@logger.info "Starting RubyDNS server (v#{RubyDNS::VERSION})..."
			
			fire(:setup)
			
			# Setup server sockets
			@interfaces.each do |spec|
				if spec.is_a?(BasicSocket)
					spec.do_not_reverse_lookup
					protocol = spec.getsockopt(Socket::SOL_SOCKET, Socket::SO_TYPE).unpack("i")[0]
					ip = spec.local_address.ip_address
					port = spec.local_address.ip_port
					
					case protocol
					when Socket::SOCK_DGRAM
						@logger.info "<> Attaching to pre-existing UDP socket #{ip}:#{port}"
						link UDPSocketHandler.new(self, UDPSocketWrapper.new(spec))
					when Socket::SOCK_STREAM
						@logger.info "<> Attaching to pre-existing TCP socket #{ip}:#{port}"
						link TCPSocketHandler.new(self, TCPServerWrapper.new(spec))
					else
						raise ArgumentError.new("Unknown socket protocol: #{protocol}")
					end
				elsif spec[0] == :udp
					@logger.info "<> Listening on #{spec.join(':')}"
					link UDPHandler.new(self, spec[1], spec[2])
				elsif spec[0] == :tcp
					@logger.info "<> Listening on #{spec.join(':')}"
					link TCPHandler.new(self, spec[1], spec[2])
				else
					raise ArgumentError.new("Invalid connection specification: #{spec.inspect}")
				end
			end
			
			fire(:start)
		end
	end
	
	# Provides the core of the RubyDNS domain-specific language (DSL). It contains a list of rules which are used to match against incoming DNS questions. These rules are used to generate responses which are either DNS resource records or failures.
	class RuleBasedServer < Server
		# Represents a single rule in the server.
		class Rule
			def initialize(pattern, callback)
				@pattern = pattern
				@callback = callback
			end
			
			# Returns true if the name and resource_class are sufficient:
			def match(name, resource_class)
				# If the pattern doesn't specify any resource classes, we implicitly pass this test:
				return true if @pattern.size < 2
				
				# Otherwise, we try to match against some specific resource classes:
				if Class === @pattern[1]
					@pattern[1] == resource_class
				else
					@pattern[1].include?(resource_class) rescue false
				end
			end
			
			# Invoke the rule, if it matches the incoming request, it is evaluated and returns `true`, otherwise returns `false`.
			def call(server, name, resource_class, transaction)
				unless match(name, resource_class)
					server.logger.debug "<#{transaction.query.id}> Resource class #{resource_class} failed to match #{@pattern[1].inspect}!"
					
					return false
				end
				
				# Does this rule match against the supplied name?
				case @pattern[0]
				when Regexp
					match_data = @pattern[0].match(name)
					
					if match_data
						server.logger.debug "<#{transaction.query.id}> Regexp pattern matched with #{match_data.inspect}."
						
						@callback[transaction, match_data]
						
						return true
					end
				when String
					if @pattern[0] == name
						server.logger.debug "<#{transaction.query.id}> String pattern matched."
						
						@callback[transaction]
						
						return true
					end
				else
					if (@pattern[0].call(name, resource_class) rescue false)
						server.logger.debug "<#{transaction.query.id}> Callable pattern matched."
						
						@callback[transaction]
						
						return true
					end
				end
				
				server.logger.debug "<#{transaction.query.id}> No pattern matched."
				
				# We failed to match the pattern.
				return false
			end
			
			def to_s
				@pattern.inspect
			end
		end
		
		# Don't wrap the block going into initialize.
		execute_block_on_receiver :initialize
		
		# Instantiate a server with a block
		#
		#	server = Server.new do
		#		match(/server.mydomain.com/, IN::A) do |transaction|
		#			transaction.respond!("1.2.3.4")
		#		end
		#	end
		#
		def initialize(options = {}, &block)
			super(options)
			
			@events = {}
			@rules = []
			@otherwise = nil
			
			if block_given?
				instance_eval(&block)
			end
		end

		attr_accessor :logger

		# This function connects a pattern with a block. A pattern is either a String or a Regex instance. Optionally, a second argument can be provided which is either a String, Symbol or Array of resource record types which the rule matches against.
		# 
		#	match("www.google.com")
		#	match("gmail.com", IN::MX)
		#	match(/g?mail.(com|org|net)/, [IN::MX, IN::A])
		#
		def match(*pattern, &block)
			@rules << Rule.new(pattern, block)
		end

		# Register a named event which may be invoked later using #fire
		#
		#	on(:start) do |server|
		#		Process::Daemon::Permissions.change_user(RUN_AS)
		#	end
		def on(event_name, &block)
			@events[event_name] = block
		end
		
		# Fire the named event, which must have been registered using on.
		def fire(event_name)
			callback = @events[event_name]
			
			if callback
				callback.call(self)
			end
		end
		
		# Specify a default block to execute if all other rules fail to match. This block is typially used to pass the request on to another server (i.e. recursive request).
		#
		#	otherwise do |transaction|
		#		transaction.passthrough!($R)
		#	end
		#
		def otherwise(&block)
			@otherwise = block
		end
		
		# If you match a rule, but decide within the rule that it isn't the correct one to use, you can call `next!` to evaluate the next rule - in other words, to continue falling down through the list of rules.
		def next!
			throw :next
		end
		
		# Give a name and a record type, try to match a rule and use it for processing the given arguments.
		def process(name, resource_class, transaction)
			@logger.debug {"<#{transaction.query.id}> Searching for #{name} #{resource_class.name}"}
			
			@rules.each do |rule|
				@logger.debug {"<#{transaction.query.id}> Checking rule #{rule}..."}
				
				catch (:next) do
					# If the rule returns true, we assume that it was successful and no further rules need to be evaluated.
					return if rule.call(self, name, resource_class, transaction)
				end
			end
			
			if @otherwise
				@otherwise.call(transaction)
			else
				@logger.warn "<#{transaction.query.id}> Failed to handle #{name} #{resource_class.name}!"
			end
		end
	end
end