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