lib/rubydns/server.rb in rubydns-0.8.5 vs lib/rubydns/server.rb in rubydns-0.9.0
- old
+ new
@@ -16,18 +16,33 @@
# 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 '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
#
@@ -35,70 +50,68 @@
# match(/server.mydomain.com/, IN::A) do |transaction|
# transaction.respond!("1.2.3.4")
# end
# end
#
- def initialize(options)
- @logger = options[:logger] || Logger.new($stderr)
+ def initialize(options = {})
+ @handlers = []
+
+ @logger = options[:logger] || Celluloid.logger
+ @interfaces = options[:listen] || DEFAULT_INTERFACES
end
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 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)
start_time = Time.now
- # 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
+ # 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
- Fiber.new do
- transaction = nil
-
- begin
- query.question.each do |question, resource_class|
- @logger.debug {"[#{query.id}] Processing question #{question} #{resource_class}..."}
-
- transaction = Transaction.new(self, query, question, resource_class, answer, options)
-
- transaction.process
- end
- rescue
- @logger.error {"[#{query.id}] Exception thrown while processing #{transaction}!"}
- RubyDNS.log_exception(@logger, $!)
-
- answer.rcode = Resolv::DNS::RCode::ServFail
+ transaction = nil
+
+ begin
+ query.question.each do |question, resource_class|
+ @logger.debug {"<#{query.id}> Processing question #{question} #{resource_class}..."}
+
+ transaction = Transaction.new(self, query, question, resource_class, response, options)
+
+ transaction.process
end
-
- yield answer
-
- end_time = Time.now
- @logger.debug {"[#{query.id}] Time to process request: #{end_time - start_time}s"}
- end.resume
+ 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.
#
@@ -111,45 +124,44 @@
#
# socket = UDPSocket.new; socket.bind("0.0.0.0", 53)
# Process::Sys.setuid(server_uid)
# INTERFACES = [socket]
#
- def run(options = {})
+ def run
@logger.info "Starting RubyDNS server (v#{RubyDNS::VERSION})..."
-
- interfaces = options[:listen] || DEFAULT_INTERFACES
-
+
fire(:setup)
-
+
# Setup server sockets
- interfaces.each do |spec|
+ @interfaces.each do |spec|
if spec.is_a?(BasicSocket)
spec.do_not_reverse_lookup
- optval = spec.getsockopt(Socket::SOL_SOCKET, Socket::SO_TYPE)
- protocol = optval.unpack("i")[0]
+ 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}"
- EventMachine.attach(spec, UDPHandler, self)
+ @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}"
- EventMachine.attach(spec, TCPHandler, self)
+ @logger.info "<> Attaching to pre-existing TCP socket #{ip}:#{port}"
+ link TCPSocketHandler.new(self, TCPServerWrapper.new(spec))
else
- @logger.error "Ignoring unknown socket protocol: #{protocol}"
+ 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
- @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
+ 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.
@@ -173,58 +185,61 @@
@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)
+ def call(server, name, resource_class, transaction)
unless match(name, resource_class)
- server.logger.debug "Resource class #{resource_class} failed to match #{@pattern[1].inspect}!"
+ 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 "Regexp pattern matched with #{match_data.inspect}."
+ server.logger.debug "<#{transaction.query.id}> Regexp pattern matched with #{match_data.inspect}."
- @callback[*args, match_data]
+ @callback[transaction, match_data]
return true
end
when String
if @pattern[0] == name
- server.logger.debug "String pattern matched."
+ server.logger.debug "<#{transaction.query.id}> String pattern matched."
- @callback[*args]
+ @callback[transaction]
return true
end
else
if (@pattern[0].call(name, resource_class) rescue false)
- server.logger.debug "Callable pattern matched."
+ server.logger.debug "<#{transaction.query.id}> Callable pattern matched."
- @callback[*args]
+ @callback[transaction]
return true
end
end
- server.logger.debug "No pattern matched."
+ 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")
@@ -287,34 +302,25 @@
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, *args)
- @logger.debug {"Searching for #{name} #{resource_class.name}"}
+ def process(name, resource_class, transaction)
+ @logger.debug {"<#{transaction.query.id}> Searching for #{name} #{resource_class.name}"}
@rules.each do |rule|
- @logger.debug {"Checking rule #{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, *args)
+ return if rule.call(self, name, resource_class, transaction)
end
end
if @otherwise
- @otherwise.call(*args)
+ @otherwise.call(transaction)
else
- @logger.warn "Failed to handle #{name} #{resource_class.name}!"
+ @logger.warn "<#{transaction.query.id}> Failed to handle #{name} #{resource_class.name}!"
end
- 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
end
end