lib/rubydns/server.rb in rubydns-0.6.7 vs lib/rubydns/server.rb in rubydns-0.7.0
- old
+ new
@@ -16,25 +16,131 @@
# 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 'fiber'
+
require 'rubydns/transaction'
require 'rubydns/extensions/logger'
module RubyDNS
- # This class provides the core of the 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 Server
+ # 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
+ @logger = Logger.new($stderr)
+ end
+
+ attr_accessor :logger
+
+ # Fire the named event as part of running the server.
+ def fire(event_name)
+ 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 a block with the current fiber. To resume processing from the block, call `fiber.resume`. You shouldn't call `fiber.resume` until after the top level block has returned.
+ def defer(&block)
+ fiber = Fiber.current
+
+ yield(fiber)
+
+ Fiber.yield
+ end
+
+ # Process an incoming DNS message. Returns a serialized message to be sent back to the client.
+ def process_query(query, options = {}, &block)
+ # Setup answer
+ answer = Resolv::DNS::Message::new(query.id)
+ answer.qr = 1 # 0 = Query, 1 = Response
+ answer.opcode = query.opcode # Type of Query; copy from query
+ answer.aa = 1 # Is this an authoritative response: 0 = No, 1 = Yes
+ answer.rd = query.rd # Is Recursion Desired, copied from query
+ answer.ra = 0 # Does name server support recursion: 0 = No, 1 = Yes
+ answer.rcode = 0 # Response code: 0 = No errors
+
+ Fiber.new do
+ transaction = nil
+
+ begin
+ query.question.each do |question, resource_class|
+ @logger.debug "Processing question #{question} #{resource_class}..."
+
+ transaction = Transaction.new(self, query, question, resource_class, answer, options)
+
+ transaction.process
+ end
+ rescue
+ @logger.error "Exception thrown while processing #{transaction}!"
+ RubyDNS.log_exception(@logger, $!)
+
+ answer.rcode = Resolv::DNS::RCode::ServFail
+ end
+
+ yield answer
+ end.resume
+ 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(options = {})
+ @logger.info "Starting RubyDNS server (v#{RubyDNS::VERSION})..."
+
+ interfaces = options[:listen] || DEFAULT_INTERFACES
+
+ fire(:setup)
+
+ # Setup server sockets
+ interfaces.each do |spec|
+ @logger.info "Listening on #{spec.join(':')}"
+ if spec[0] == :udp
+ EventMachine.open_datagram_socket(spec[1], spec[2], UDPHandler, self)
+ elsif spec[0] == :tcp
+ EventMachine.start_server(spec[1], spec[2], TCPHandler, self)
+ 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:
@@ -43,38 +149,50 @@
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, *args)
unless match(name, resource_class)
server.logger.debug "Resource class #{resource_class} failed to match #{@pattern[1].inspect}!"
return false
end
- # Match succeeded against name?
+ # 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 "Regexp pattern matched with #{match_data.inspect}."
- return @callback[*args, match_data]
+
+ @callback[*args, match_data]
+
+ return true
end
when String
if @pattern[0] == name
server.logger.debug "String pattern matched."
- return @callback[*args]
+
+ @callback[*args]
+
+ return true
end
else
if (@pattern[0].call(name, resource_class) rescue false)
server.logger.debug "Callable pattern matched."
- return @callback[*args]
+
+ @callback[*args]
+
+ return true
end
end
server.logger.debug "No pattern matched."
+
# We failed to match the pattern.
return false
end
def to_s
@@ -82,47 +200,45 @@
end
end
# 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
+ # server = Server.new do
+ # match(/server.mydomain.com/, IN::A) do |transaction|
+ # transaction.respond!("1.2.3.4")
+ # end
+ # end
#
def initialize(&block)
+ super()
+
@events = {}
@rules = []
@otherwise = nil
-
- @logger = Logger.new($stderr)
-
+
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.
+ # 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])
+ # 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|
- # RExec.change_user(RUN_AS)
- # end
+ #
+ # on(:start) do |server|
+ # RExec.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.
@@ -132,110 +248,83 @@
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).
+ # 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
+ # 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.
- #
- # If a rule returns false, it is considered that the rule failed and
- # futher matching is carried out.
+
+ # 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, *args)
@logger.debug "Searching for #{name} #{resource_class.name}"
-
+
@rules.each do |rule|
@logger.debug "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 true if rule.call(self, name, resource_class, *args)
+ return if rule.call(self, name, resource_class, *args)
end
end
-
+
if @otherwise
@otherwise.call(*args)
else
@logger.warn "Failed to handle #{name} #{resource_class.name}!"
end
end
-
- # Process an incoming DNS message. Returns a serialized message to be
- # sent back to the client.
+
+ # Process a block with the current fiber. To resume processing from the block, call `fiber.resume`. You shouldn't call `fiber.resume` until after the top level block has returned.
+ def defer(&block)
+ fiber = Fiber.current
+
+ yield(fiber)
+
+ Fiber.yield
+ end
+
+ # Process an incoming DNS message. Returns a serialized message to be sent back to the client.
def process_query(query, options = {}, &block)
# Setup answer
answer = Resolv::DNS::Message::new(query.id)
answer.qr = 1 # 0 = Query, 1 = Response
answer.opcode = query.opcode # Type of Query; copy from query
answer.aa = 1 # Is this an authoritative response: 0 = No, 1 = Yes
answer.rd = query.rd # Is Recursion Desired, copied from query
answer.ra = 0 # Does name server support recursion: 0 = No, 1 = Yes
answer.rcode = 0 # Response code: 0 = No errors
-
- # 1/ This chain contains a reverse list of question lambdas.
- chain = []
-
- # 4/ Finally, the answer is given back to the calling block:
- chain << lambda do
- @logger.debug "Passing answer back to caller..."
- yield answer
- end
-
- # There may be multiple questions per query
- query.question.reverse.each do |question, resource_class|
- next_link = chain.last
-
- chain << lambda do
- @logger.debug "Processing question #{question} #{resource_class}..."
-
- transaction = Transaction.new(self, query, question, resource_class, answer, options)
-
- # Call the next link in the chain:
- transaction.callback do
- # 3/ ... which calls the previous item in the chain, i.e. the next question to be answered:
- next_link.call
- end
-
- # If there was an error, log it and fail:
- transaction.errback do |response|
- if Exception === response
- @logger.error "Exception thrown while processing #{transaction}!"
- RubyDNS.log_exception(@logger, response)
- else
- @logger.error "Failure while processing #{transaction}!"
- @logger.error "#{response.inspect}"
- end
-
- answer.rcode = Resolv::DNS::RCode::ServFail
-
- chain.first.call
- end
-
- begin
- # Transaction.process will call succeed if it wasn't deferred:
+
+ Fiber.new do
+ transaction = nil
+
+ begin
+ query.question.each do |question, resource_class|
+ @logger.debug "Processing question #{question} #{resource_class}..."
+
+ transaction = Transaction.new(self, query, question, resource_class, answer, options)
+
transaction.process
- rescue
- transaction.fail($!)
end
+ rescue
+ @logger.error "Exception thrown while processing #{transaction}!"
+ RubyDNS.log_exception(@logger, $!)
+
+ answer.rcode = Resolv::DNS::RCode::ServFail
end
- end
-
- # 2/ We call the last lambda...
- chain.last.call
+
+ yield answer
+ end.resume
end
end
end