rubydns 0.8.5 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +4 -0
- data/Gemfile +0 -4
- data/README.md +16 -8
- data/Rakefile +6 -6
- data/{test/examples → examples}/dropping-dns.rb +0 -0
- data/lib/rubydns/handler.rb +133 -102
- data/lib/rubydns/message.rb +0 -1
- data/lib/rubydns/resolver.rb +135 -187
- data/lib/rubydns/server.rb +92 -86
- data/lib/rubydns/transaction.rb +24 -48
- data/lib/rubydns/{binary_string.rb → transport.rb} +38 -0
- data/lib/rubydns/version.rb +1 -1
- data/lib/rubydns.rb +15 -8
- data/rubydns.gemspec +5 -2
- data/{test/test_daemon.rb → spec/rubydns/daemon_spec.rb} +36 -48
- data/{test → spec/rubydns}/hosts.txt +0 -0
- data/{test/test_rules.rb → spec/rubydns/message_spec.rb} +26 -44
- data/spec/rubydns/passthrough_spec.rb +78 -0
- data/spec/rubydns/resolver_performance_spec.rb +110 -0
- data/spec/rubydns/resolver_spec.rb +144 -0
- data/spec/rubydns/rules_spec.rb +74 -0
- data/{test/performance → spec/rubydns/server}/benchmark.rb +0 -0
- data/{test/performance → spec/rubydns/server}/bind9/generate-local.rb +0 -0
- data/{test/performance → spec/rubydns/server}/bind9/local.zone +0 -0
- data/{test/performance → spec/rubydns/server}/bind9/named.conf +0 -0
- data/spec/rubydns/server/bind9/named.run +0 -0
- data/{test/performance → spec/rubydns/server}/million.rb +1 -3
- data/spec/rubydns/server/rubydns.stackprof +0 -0
- data/spec/rubydns/server_performance_spec.rb +136 -0
- data/spec/rubydns/slow_server_spec.rb +89 -0
- data/spec/rubydns/socket_spec.rb +77 -0
- data/{test/test_system.rb → spec/rubydns/system_spec.rb} +28 -22
- data/spec/rubydns/transaction_spec.rb +64 -0
- data/{test/test_truncation.rb → spec/rubydns/truncation_spec.rb} +22 -48
- metadata +91 -54
- data/test/examples/fortune-dns.rb +0 -107
- data/test/examples/geoip-dns.rb +0 -76
- data/test/examples/soa-dns.rb +0 -82
- data/test/examples/test-dns-1.rb +0 -77
- data/test/examples/test-dns-2.rb +0 -83
- data/test/examples/wikipedia-dns.rb +0 -112
- data/test/test_message.rb +0 -65
- data/test/test_passthrough.rb +0 -120
- data/test/test_resolver.rb +0 -106
- data/test/test_resolver_performance.rb +0 -123
- data/test/test_server_performance.rb +0 -134
- data/test/test_slow_server.rb +0 -125
    
        data/lib/rubydns/resolver.rb
    CHANGED
    
    | @@ -18,19 +18,24 @@ | |
| 18 18 | 
             
            # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
         | 
| 19 19 | 
             
            # THE SOFTWARE.
         | 
| 20 20 |  | 
| 21 | 
            -
            require_relative ' | 
| 22 | 
            -
            require_relative 'binary_string'
         | 
| 21 | 
            +
            require_relative 'handler'
         | 
| 23 22 |  | 
| 24 23 | 
             
            require 'securerandom'
         | 
| 24 | 
            +
            require 'celluloid/io'
         | 
| 25 25 |  | 
| 26 26 | 
             
            module RubyDNS
         | 
| 27 27 | 
             
            	class InvalidProtocolError < StandardError
         | 
| 28 28 | 
             
            	end
         | 
| 29 29 |  | 
| 30 | 
            +
            	class InvalidResponseError < StandardError
         | 
| 31 | 
            +
            	end
         | 
| 32 | 
            +
            	
         | 
| 30 33 | 
             
            	class ResolutionFailure < StandardError
         | 
| 31 34 | 
             
            	end
         | 
| 32 35 |  | 
| 33 36 | 
             
            	class Resolver
         | 
| 37 | 
            +
            		include Celluloid::IO
         | 
| 38 | 
            +
            		
         | 
| 34 39 | 
             
            		# Servers are specified in the same manor as options[:listen], e.g.
         | 
| 35 40 | 
             
            		#   [:tcp/:udp, address, port]
         | 
| 36 41 | 
             
            		# In the case of multiple servers, they will be checked in sequence.
         | 
| @@ -38,6 +43,8 @@ module RubyDNS | |
| 38 43 | 
             
            			@servers = servers
         | 
| 39 44 |  | 
| 40 45 | 
             
            			@options = options
         | 
| 46 | 
            +
            			
         | 
| 47 | 
            +
            			@logger = options[:logger] || Celluloid.logger
         | 
| 41 48 | 
             
            		end
         | 
| 42 49 |  | 
| 43 50 | 
             
            		# Provides the next sequence identification number which is used to keep track of DNS messages.
         | 
| @@ -47,54 +54,138 @@ module RubyDNS | |
| 47 54 | 
             
            		end
         | 
| 48 55 |  | 
| 49 56 | 
             
            		# Look up a named resource of the given resource_class.
         | 
| 50 | 
            -
            		def query(name, resource_class = Resolv::DNS::Resource::IN::A | 
| 57 | 
            +
            		def query(name, resource_class = Resolv::DNS::Resource::IN::A)
         | 
| 51 58 | 
             
            			message = Resolv::DNS::Message.new(next_id!)
         | 
| 52 59 | 
             
            			message.rd = 1
         | 
| 53 60 | 
             
            			message.add_question name, resource_class
         | 
| 54 61 |  | 
| 55 | 
            -
            			 | 
| 62 | 
            +
            			dispatch_request(message)
         | 
| 56 63 | 
             
            		end
         | 
| 57 | 
            -
             | 
| 58 | 
            -
            		 | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
             | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 66 | 
            -
             | 
| 67 | 
            -
             | 
| 68 | 
            -
            				case response
         | 
| 69 | 
            -
            				when Message
         | 
| 70 | 
            -
            					yield response.answer.select{|record| record[0].to_s == name}.collect{|record| record[2].address}
         | 
| 71 | 
            -
            				else
         | 
| 72 | 
            -
            					yield []
         | 
| 64 | 
            +
            		
         | 
| 65 | 
            +
            		# Yields a list of `Resolv::IPv4` and `Resolv::IPv6` addresses for the given `name` and `resource_class`. Raises a ResolutionFailure if no severs respond.
         | 
| 66 | 
            +
            		def addresses_for(name, resource_class = Resolv::DNS::Resource::IN::A, options = {})
         | 
| 67 | 
            +
            			(options[:retries] || 5).times do
         | 
| 68 | 
            +
            				response = query(name, resource_class)
         | 
| 69 | 
            +
            				
         | 
| 70 | 
            +
            				if response
         | 
| 71 | 
            +
            					# Resolv::DNS::Name doesn't retain the trailing dot.
         | 
| 72 | 
            +
            					name = name.sub(/\.$/, '')
         | 
| 73 | 
            +
            					
         | 
| 74 | 
            +
            					return response.answer.select{|record| record[0].to_s == name}.collect{|record| record[2].address}
         | 
| 73 75 | 
             
            				end
         | 
| 76 | 
            +
            				
         | 
| 77 | 
            +
            				# Wait 10ms before trying again:
         | 
| 78 | 
            +
            				sleep 0.01
         | 
| 74 79 | 
             
            			end
         | 
| 80 | 
            +
            			
         | 
| 81 | 
            +
            			abort ResolutionFailure.new("No server replied.")
         | 
| 75 82 | 
             
            		end
         | 
| 76 | 
            -
             | 
| 77 | 
            -
            		 | 
| 78 | 
            -
             | 
| 79 | 
            -
             | 
| 83 | 
            +
            		
         | 
| 84 | 
            +
            		def request_timeout
         | 
| 85 | 
            +
            			@options[:timeout] || 1
         | 
| 86 | 
            +
            		end
         | 
| 87 | 
            +
            		
         | 
| 88 | 
            +
            		# Send the message to available servers. If no servers respond correctly, nil is returned. This result indicates a failure of the resolver to correctly contact any server and get a valid response.
         | 
| 89 | 
            +
            		def dispatch_request(message)
         | 
| 90 | 
            +
            			request = Request.new(message, @servers)
         | 
| 80 91 |  | 
| 81 | 
            -
            			 | 
| 82 | 
            -
            				request  | 
| 83 | 
            -
            				
         | 
| 84 | 
            -
            				request.callback do |message|
         | 
| 85 | 
            -
            					yield message
         | 
| 86 | 
            -
            				end
         | 
| 92 | 
            +
            			request.each do |server|
         | 
| 93 | 
            +
            				@logger.debug "[#{message.id}] Sending request to server #{server.inspect}" if @logger
         | 
| 87 94 |  | 
| 88 | 
            -
            				 | 
| 89 | 
            -
            					 | 
| 95 | 
            +
            				begin
         | 
| 96 | 
            +
            					response = nil
         | 
| 97 | 
            +
            					
         | 
| 98 | 
            +
            					timeout(request_timeout) do
         | 
| 99 | 
            +
            						response = try_server(request, server)
         | 
| 100 | 
            +
            					end
         | 
| 90 101 |  | 
| 91 | 
            -
            					 | 
| 102 | 
            +
            					if valid_response(message, response)
         | 
| 103 | 
            +
            						return response
         | 
| 104 | 
            +
            					end
         | 
| 105 | 
            +
            				rescue Task::TimeoutError
         | 
| 106 | 
            +
            					@logger.debug "[#{message.id}] Request timed out!" if @logger
         | 
| 107 | 
            +
            				rescue InvalidResponseError
         | 
| 108 | 
            +
            					@logger.warn "[#{message.id}] Invalid response from network: #{$!}!" if @logger
         | 
| 109 | 
            +
            				rescue DecodeError
         | 
| 110 | 
            +
            					@logger.warn "[#{message.id}] Error while decoding data from network: #{$!}!" if @logger
         | 
| 111 | 
            +
            				rescue IOError
         | 
| 112 | 
            +
            					@logger.warn "[#{message.id}] Error while reading from network: #{$!}!" if @logger
         | 
| 92 113 | 
             
            				end
         | 
| 93 | 
            -
            				
         | 
| 94 | 
            -
            				request.run!
         | 
| 95 114 | 
             
            			end
         | 
| 96 115 |  | 
| 97 | 
            -
            			 | 
| 116 | 
            +
            			return nil
         | 
| 117 | 
            +
            		end
         | 
| 118 | 
            +
            		
         | 
| 119 | 
            +
            		private
         | 
| 120 | 
            +
            		
         | 
| 121 | 
            +
            		def try_server(request, server)
         | 
| 122 | 
            +
            			case server[0]
         | 
| 123 | 
            +
            			when :udp
         | 
| 124 | 
            +
            				try_udp_server(request, server[1], server[2])
         | 
| 125 | 
            +
            			when :tcp
         | 
| 126 | 
            +
            				try_tcp_server(request, server[1], server[2])
         | 
| 127 | 
            +
            			else
         | 
| 128 | 
            +
            				raise InvalidProtocolError.new(server)
         | 
| 129 | 
            +
            			end
         | 
| 130 | 
            +
            		end
         | 
| 131 | 
            +
            		
         | 
| 132 | 
            +
            		def valid_response(message, response)
         | 
| 133 | 
            +
            			if response.tc != 0
         | 
| 134 | 
            +
            				@logger.warn "[#{message.id}] Received truncated response!" if @logger
         | 
| 135 | 
            +
            			elsif response.id != message.id
         | 
| 136 | 
            +
            				@logger.warn "[#{message.id}] Received response with incorrect message id: #{response.id}!" if @logger
         | 
| 137 | 
            +
            			else
         | 
| 138 | 
            +
            				@logger.debug "[#{message.id}] Received valid response with #{response.answer.count} answer(s)." if @logger
         | 
| 139 | 
            +
            		
         | 
| 140 | 
            +
            				return true
         | 
| 141 | 
            +
            			end
         | 
| 142 | 
            +
            			
         | 
| 143 | 
            +
            			return false
         | 
| 144 | 
            +
            		end
         | 
| 145 | 
            +
            		
         | 
| 146 | 
            +
            		def try_udp_server(request, host, port)
         | 
| 147 | 
            +
            			socket = UDPSocket.new
         | 
| 148 | 
            +
            			
         | 
| 149 | 
            +
            			socket.send(request.packet, 0, host, port)
         | 
| 150 | 
            +
            			
         | 
| 151 | 
            +
            			data, (_, remote_port) = socket.recvfrom(UDP_TRUNCATION_SIZE)
         | 
| 152 | 
            +
            			# Need to check host, otherwise security issue.
         | 
| 153 | 
            +
            			
         | 
| 154 | 
            +
            			# May indicate some kind of spoofing attack:
         | 
| 155 | 
            +
            			if port != remote_port
         | 
| 156 | 
            +
            				raise InvalidResponseError.new("Data was not received from correct remote port (#{port} != #{remote_port})")
         | 
| 157 | 
            +
            			end
         | 
| 158 | 
            +
            			
         | 
| 159 | 
            +
            			message = RubyDNS::decode_message(data)
         | 
| 160 | 
            +
            		ensure
         | 
| 161 | 
            +
            			socket.close if socket
         | 
| 162 | 
            +
            		end
         | 
| 163 | 
            +
            		
         | 
| 164 | 
            +
            		def try_tcp_server(request, host, port)
         | 
| 165 | 
            +
            			begin
         | 
| 166 | 
            +
            				socket = TCPSocket.new(host, port)
         | 
| 167 | 
            +
            			rescue Errno::EALREADY
         | 
| 168 | 
            +
            				raise IOError.new("Could not connect to remote host!")
         | 
| 169 | 
            +
            			end
         | 
| 170 | 
            +
            			
         | 
| 171 | 
            +
            			StreamTransport.write_chunk(socket, request.packet)
         | 
| 172 | 
            +
            			
         | 
| 173 | 
            +
            			input_data = StreamTransport.read_chunk(socket)
         | 
| 174 | 
            +
            			
         | 
| 175 | 
            +
            			message = RubyDNS::decode_message(input_data)
         | 
| 176 | 
            +
            		rescue Errno::ECONNREFUSED => error
         | 
| 177 | 
            +
            			raise IOError.new(error.message)
         | 
| 178 | 
            +
            		rescue Errno::EPIPE => error
         | 
| 179 | 
            +
            			raise IOError.new(error.message)
         | 
| 180 | 
            +
            		rescue Errno::ECONNRESET => error
         | 
| 181 | 
            +
            			raise IOError.new(error.message)
         | 
| 182 | 
            +
            		ensure
         | 
| 183 | 
            +
            			socket.close if socket
         | 
| 184 | 
            +
            		end
         | 
| 185 | 
            +
            		
         | 
| 186 | 
            +
            		# Manages a single DNS question message across one or more servers.
         | 
| 187 | 
            +
            		class Request
         | 
| 188 | 
            +
            			def initialize(message, servers)
         | 
| 98 189 | 
             
            				@message = message
         | 
| 99 190 | 
             
            				@packet = message.encode
         | 
| 100 191 |  | 
| @@ -104,167 +195,24 @@ module RubyDNS | |
| 104 195 | 
             
            				if @packet.bytesize > UDP_TRUNCATION_SIZE
         | 
| 105 196 | 
             
            					@servers.delete_if{|server| server[0] == :udp}
         | 
| 106 197 | 
             
            				end
         | 
| 107 | 
            -
            				
         | 
| 108 | 
            -
            				# Measured in seconds:
         | 
| 109 | 
            -
            				@timeout = options[:timeout] || 1
         | 
| 110 | 
            -
            				
         | 
| 111 | 
            -
            				@logger = options[:logger]
         | 
| 112 198 | 
             
            			end
         | 
| 113 199 |  | 
| 114 200 | 
             
            			attr :message
         | 
| 115 201 | 
             
            			attr :packet
         | 
| 116 202 | 
             
            			attr :logger
         | 
| 117 203 |  | 
| 118 | 
            -
            			def  | 
| 119 | 
            -
            				 | 
| 120 | 
            -
             | 
| 121 | 
            -
            			
         | 
| 122 | 
            -
            			# Once either an exception or message is received, we update the status of this request.
         | 
| 123 | 
            -
            			def process_response!(response)
         | 
| 124 | 
            -
            				finish_request!
         | 
| 125 | 
            -
            				
         | 
| 126 | 
            -
            				if Exception === response
         | 
| 127 | 
            -
            					@logger.warn "[#{@message.id}] Failure while processing response #{response}!" if @logger
         | 
| 128 | 
            -
            					RubyDNS.log_exception(@logger, response) if @logger
         | 
| 129 | 
            -
            					
         | 
| 130 | 
            -
            					try_next_server!
         | 
| 131 | 
            -
            				elsif response.tc != 0
         | 
| 132 | 
            -
            					@logger.warn "[#{@message.id}] Received truncated response!" if @logger
         | 
| 204 | 
            +
            			def each(&block)
         | 
| 205 | 
            +
            				@servers.each do |server|
         | 
| 206 | 
            +
            					next if @packet.bytesize > UDP_TRUNCATION_SIZE
         | 
| 133 207 |  | 
| 134 | 
            -
            					 | 
| 135 | 
            -
            				elsif response.id != @message.id
         | 
| 136 | 
            -
            					@logger.warn "[#{@message.id}] Received response with incorrect message id: #{response.id}" if @logger
         | 
| 137 | 
            -
            					
         | 
| 138 | 
            -
            					try_next_server!
         | 
| 139 | 
            -
            				else
         | 
| 140 | 
            -
            					@logger.debug "[#{@message.id}] Received valid response #{response.inspect}" if @logger
         | 
| 141 | 
            -
            					
         | 
| 142 | 
            -
            					succeed response
         | 
| 143 | 
            -
            				end
         | 
| 144 | 
            -
            			end
         | 
| 145 | 
            -
            			
         | 
| 146 | 
            -
            			private
         | 
| 147 | 
            -
            			
         | 
| 148 | 
            -
            			# Closes any connections and cancels any timeout.
         | 
| 149 | 
            -
            			def finish_request!
         | 
| 150 | 
            -
            				cancel_timeout
         | 
| 151 | 
            -
            				
         | 
| 152 | 
            -
            				# Cancel an existing request if it is in flight:
         | 
| 153 | 
            -
            				if @request
         | 
| 154 | 
            -
            					@request.close_connection
         | 
| 155 | 
            -
            					@request = nil
         | 
| 156 | 
            -
            				end
         | 
| 157 | 
            -
            			end
         | 
| 158 | 
            -
            			
         | 
| 159 | 
            -
            			def try_next_server!
         | 
| 160 | 
            -
            				if @servers.size > 0
         | 
| 161 | 
            -
            					@server = @servers.shift
         | 
| 162 | 
            -
            					
         | 
| 163 | 
            -
            					@logger.debug "[#{@message.id}] Sending request to server #{@server.inspect}" if @logger
         | 
| 164 | 
            -
            					
         | 
| 165 | 
            -
            					# We make requests one at a time to the given server, naturally the servers are ordered in terms of priority.
         | 
| 166 | 
            -
            					case @server[0]
         | 
| 167 | 
            -
            					when :udp
         | 
| 168 | 
            -
            						@request = UDPRequestHandler.open(@server[1], @server[2], self)
         | 
| 169 | 
            -
            					when :tcp
         | 
| 170 | 
            -
            						@request = TCPRequestHandler.open(@server[1], @server[2], self)
         | 
| 171 | 
            -
            					else
         | 
| 172 | 
            -
            						raise InvalidProtocolError.new(@server)
         | 
| 173 | 
            -
            					end
         | 
| 174 | 
            -
            					
         | 
| 175 | 
            -
            					# Setting up the timeout...
         | 
| 176 | 
            -
            					timeout(@timeout)
         | 
| 177 | 
            -
            				else
         | 
| 178 | 
            -
            					fail ResolutionFailure.new("No available servers responded to the request.")
         | 
| 179 | 
            -
            				end
         | 
| 180 | 
            -
            			end
         | 
| 181 | 
            -
            			
         | 
| 182 | 
            -
            			def timeout seconds
         | 
| 183 | 
            -
            				cancel_timeout
         | 
| 184 | 
            -
            				
         | 
| 185 | 
            -
            				@deferred_timeout = EventMachine::Timer.new(seconds) do
         | 
| 186 | 
            -
            					@logger.debug "[#{@message.id}] Request timed out!" if @logger
         | 
| 187 | 
            -
            					
         | 
| 188 | 
            -
            					finish_request!
         | 
| 189 | 
            -
            					
         | 
| 190 | 
            -
            					try_next_server!
         | 
| 191 | 
            -
            				end
         | 
| 192 | 
            -
            			end
         | 
| 193 | 
            -
            			
         | 
| 194 | 
            -
            			module UDPRequestHandler
         | 
| 195 | 
            -
            				def self.open(host, port, request)
         | 
| 196 | 
            -
            					# Open a datagram socket... a random socket chosen by the OS by specifying 0 for the port:
         | 
| 197 | 
            -
            					EventMachine::open_datagram_socket('', 0, self, request, host, port)
         | 
| 198 | 
            -
            				end
         | 
| 199 | 
            -
            				
         | 
| 200 | 
            -
            				def initialize(request, host, port)
         | 
| 201 | 
            -
            					@request = request
         | 
| 202 | 
            -
            					@host = host
         | 
| 203 | 
            -
            					@port = port
         | 
| 204 | 
            -
            				end
         | 
| 205 | 
            -
            				
         | 
| 206 | 
            -
            				def post_init
         | 
| 207 | 
            -
            					# Sending question to remote DNS server...
         | 
| 208 | 
            -
            					send_datagram(@request.packet, @host, @port)
         | 
| 209 | 
            -
            				end
         | 
| 210 | 
            -
            				
         | 
| 211 | 
            -
            				def receive_data(data)
         | 
| 212 | 
            -
            					# local_port, local_ip = Socket.unpack_sockaddr_in(get_sockname)
         | 
| 213 | 
            -
            					# puts "Socket name: #{local_ip}:#{local_port}"
         | 
| 214 | 
            -
            					
         | 
| 215 | 
            -
            					# Receiving response from remote DNS server...
         | 
| 216 | 
            -
            					message = RubyDNS::decode_message(data)
         | 
| 217 | 
            -
            					
         | 
| 218 | 
            -
            					# The message id must match, and it can't be truncated:
         | 
| 219 | 
            -
            					@request.process_response!(message)
         | 
| 220 | 
            -
            				rescue Resolv::DNS::DecodeError => error
         | 
| 221 | 
            -
            					@request.process_response!(error)
         | 
| 208 | 
            +
            					yield server
         | 
| 222 209 | 
             
            				end
         | 
| 223 210 | 
             
            			end
         | 
| 224 | 
            -
            			
         | 
| 225 | 
            -
            			module TCPRequestHandler
         | 
| 226 | 
            -
            				def self.open(host, port, request)
         | 
| 227 | 
            -
            					EventMachine::connect(host, port, TCPRequestHandler, request)
         | 
| 228 | 
            -
            				end
         | 
| 229 | 
            -
            				
         | 
| 230 | 
            -
            				def initialize(request)
         | 
| 231 | 
            -
            					@request = request
         | 
| 232 | 
            -
            					@buffer = nil
         | 
| 233 | 
            -
            					@length = nil
         | 
| 234 | 
            -
            				end
         | 
| 235 | 
            -
            				
         | 
| 236 | 
            -
            				def post_init
         | 
| 237 | 
            -
            					data = @request.packet
         | 
| 238 | 
            -
            					
         | 
| 239 | 
            -
            					send_data([data.bytesize].pack('n'))
         | 
| 240 | 
            -
            					send_data data
         | 
| 241 | 
            -
            				end
         | 
| 242 | 
            -
            				
         | 
| 243 | 
            -
            				def receive_data(data)
         | 
| 244 | 
            -
            					# We buffer data until we've received the entire packet:
         | 
| 245 | 
            -
            					@buffer ||= BinaryStringIO.new
         | 
| 246 | 
            -
            					@buffer.write(data)
         | 
| 247 211 |  | 
| 248 | 
            -
             | 
| 249 | 
            -
             | 
| 250 | 
            -
             | 
| 251 | 
            -
            						@length = @buffer.string.byteslice(0, 2).unpack('n')[0]
         | 
| 252 | 
            -
            					end
         | 
| 253 | 
            -
             | 
| 254 | 
            -
            					# If we know what the length is, and we've got that much data, we can decode the message:
         | 
| 255 | 
            -
            					if @length != nil and @buffer.size >= (@length + 2)
         | 
| 256 | 
            -
            						data = @buffer.string.byteslice(2, @length)
         | 
| 257 | 
            -
            						
         | 
| 258 | 
            -
            						message = RubyDNS::decode_message(data)
         | 
| 259 | 
            -
            						
         | 
| 260 | 
            -
            						@request.process_response!(message)
         | 
| 261 | 
            -
            					end
         | 
| 262 | 
            -
            					
         | 
| 263 | 
            -
            					# If we have received more data than expected, should this be an error?
         | 
| 264 | 
            -
            				rescue Resolv::DNS::DecodeError => error
         | 
| 265 | 
            -
            					@request.process_response!(error)
         | 
| 266 | 
            -
            				end
         | 
| 212 | 
            +
            			def update_id!(id)
         | 
| 213 | 
            +
            				@message.id = id
         | 
| 214 | 
            +
            				@packet = @message.encode
         | 
| 267 215 | 
             
            			end
         | 
| 268 216 | 
             
            		end
         | 
| 269 217 | 
             
            	end
         | 
| 270 | 
            -
            end
         | 
| 218 | 
            +
            end
         | 
    
        data/lib/rubydns/server.rb
    CHANGED
    
    | @@ -18,14 +18,29 @@ | |
| 18 18 | 
             
            # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
         | 
| 19 19 | 
             
            # THE SOFTWARE.
         | 
| 20 20 |  | 
| 21 | 
            -
            require ' | 
| 21 | 
            +
            require 'celluloid/io'
         | 
| 22 22 |  | 
| 23 23 | 
             
            require_relative 'transaction'
         | 
| 24 24 | 
             
            require_relative 'logger'
         | 
| 25 25 |  | 
| 26 26 | 
             
            module RubyDNS
         | 
| 27 | 
            +
            	class UDPSocketWrapper < Celluloid::IO::UDPSocket
         | 
| 28 | 
            +
            		def initialize(socket)
         | 
| 29 | 
            +
            			@socket = socket
         | 
| 30 | 
            +
            		end
         | 
| 31 | 
            +
            	end
         | 
| 32 | 
            +
            	
         | 
| 33 | 
            +
            	class TCPServerWrapper < Celluloid::IO::TCPServer
         | 
| 34 | 
            +
            		def initialize(server)
         | 
| 35 | 
            +
            			@server = server
         | 
| 36 | 
            +
            		end
         | 
| 37 | 
            +
            	end
         | 
| 27 38 |  | 
| 28 39 | 
             
            	class Server
         | 
| 40 | 
            +
            		include Celluloid::IO
         | 
| 41 | 
            +
            		
         | 
| 42 | 
            +
            		finalizer :shutdown
         | 
| 43 | 
            +
            		
         | 
| 29 44 | 
             
            		# The default server interfaces
         | 
| 30 45 | 
             
            		DEFAULT_INTERFACES = [[:udp, "0.0.0.0", 53], [:tcp, "0.0.0.0", 53]]
         | 
| 31 46 |  | 
| @@ -37,8 +52,11 @@ module RubyDNS | |
| 37 52 | 
             
            		#		end
         | 
| 38 53 | 
             
            		#	end
         | 
| 39 54 | 
             
            		#
         | 
| 40 | 
            -
            		def initialize(options)
         | 
| 41 | 
            -
            			@ | 
| 55 | 
            +
            		def initialize(options = {})
         | 
| 56 | 
            +
            			@handlers = []
         | 
| 57 | 
            +
            			
         | 
| 58 | 
            +
            			@logger = options[:logger] || Celluloid.logger
         | 
| 59 | 
            +
            			@interfaces = options[:listen] || DEFAULT_INTERFACES
         | 
| 42 60 | 
             
            		end
         | 
| 43 61 |  | 
| 44 62 | 
             
            		attr_accessor :logger
         | 
| @@ -47,56 +65,51 @@ module RubyDNS | |
| 47 65 | 
             
            		def fire(event_name)
         | 
| 48 66 | 
             
            		end
         | 
| 49 67 |  | 
| 68 | 
            +
            		def shutdown
         | 
| 69 | 
            +
            			fire(:stop)
         | 
| 70 | 
            +
            		end
         | 
| 71 | 
            +
            		
         | 
| 50 72 | 
             
            		# Give a name and a record type, try to match a rule and use it for processing the given arguments.
         | 
| 51 73 | 
             
            		def process(name, resource_class, transaction)
         | 
| 52 74 | 
             
            			raise NotImplementedError.new
         | 
| 53 75 | 
             
            		end
         | 
| 54 76 |  | 
| 55 | 
            -
            		# 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.
         | 
| 56 | 
            -
            		def defer(&block)
         | 
| 57 | 
            -
            			fiber = Fiber.current
         | 
| 58 | 
            -
            			
         | 
| 59 | 
            -
            			yield(fiber)
         | 
| 60 | 
            -
            			
         | 
| 61 | 
            -
            			Fiber.yield
         | 
| 62 | 
            -
            		end
         | 
| 63 | 
            -
            		
         | 
| 64 77 | 
             
            		# Process an incoming DNS message. Returns a serialized message to be sent back to the client.
         | 
| 65 78 | 
             
            		def process_query(query, options = {}, &block)
         | 
| 66 79 | 
             
            			start_time = Time.now
         | 
| 67 80 |  | 
| 68 | 
            -
            			# Setup  | 
| 69 | 
            -
            			 | 
| 70 | 
            -
            			 | 
| 71 | 
            -
            			 | 
| 72 | 
            -
            			 | 
| 73 | 
            -
            			 | 
| 74 | 
            -
            			 | 
| 75 | 
            -
            			 | 
| 81 | 
            +
            			# Setup response
         | 
| 82 | 
            +
            			response = Resolv::DNS::Message::new(query.id)
         | 
| 83 | 
            +
            			response.qr = 1                 # 0 = Query, 1 = Response
         | 
| 84 | 
            +
            			response.opcode = query.opcode  # Type of Query; copy from query
         | 
| 85 | 
            +
            			response.aa = 1                 # Is this an authoritative response: 0 = No, 1 = Yes
         | 
| 86 | 
            +
            			response.rd = query.rd          # Is Recursion Desired, copied from query
         | 
| 87 | 
            +
            			response.ra = 0                 # Does name server support recursion: 0 = No, 1 = Yes
         | 
| 88 | 
            +
            			response.rcode = 0              # Response code: 0 = No errors
         | 
| 76 89 |  | 
| 77 | 
            -
            			 | 
| 78 | 
            -
             | 
| 79 | 
            -
             | 
| 80 | 
            -
            				 | 
| 81 | 
            -
            					query. | 
| 82 | 
            -
             | 
| 83 | 
            -
             | 
| 84 | 
            -
             | 
| 85 | 
            -
             | 
| 86 | 
            -
            						transaction.process
         | 
| 87 | 
            -
            					end
         | 
| 88 | 
            -
            				rescue
         | 
| 89 | 
            -
            					@logger.error {"[#{query.id}] Exception thrown while processing #{transaction}!"}
         | 
| 90 | 
            -
            					RubyDNS.log_exception(@logger, $!)
         | 
| 91 | 
            -
            				
         | 
| 92 | 
            -
            					answer.rcode = Resolv::DNS::RCode::ServFail
         | 
| 90 | 
            +
            			transaction = nil
         | 
| 91 | 
            +
            			
         | 
| 92 | 
            +
            			begin
         | 
| 93 | 
            +
            				query.question.each do |question, resource_class|
         | 
| 94 | 
            +
            					@logger.debug {"<#{query.id}> Processing question #{question} #{resource_class}..."}
         | 
| 95 | 
            +
            			
         | 
| 96 | 
            +
            					transaction = Transaction.new(self, query, question, resource_class, response, options)
         | 
| 97 | 
            +
            					
         | 
| 98 | 
            +
            					transaction.process
         | 
| 93 99 | 
             
            				end
         | 
| 94 | 
            -
             | 
| 95 | 
            -
            				 | 
| 96 | 
            -
             | 
| 97 | 
            -
            				 | 
| 98 | 
            -
            				@logger | 
| 99 | 
            -
            			 | 
| 100 | 
            +
            			rescue Celluloid::ResumableError
         | 
| 101 | 
            +
            				raise
         | 
| 102 | 
            +
            			rescue StandardError => error
         | 
| 103 | 
            +
            				@logger.error "<#{query.id}> Exception thrown while processing #{transaction}!"
         | 
| 104 | 
            +
            				RubyDNS.log_exception(@logger, error)
         | 
| 105 | 
            +
            			
         | 
| 106 | 
            +
            				response.rcode = Resolv::DNS::RCode::ServFail
         | 
| 107 | 
            +
            			end
         | 
| 108 | 
            +
            			
         | 
| 109 | 
            +
            			end_time = Time.now
         | 
| 110 | 
            +
            			@logger.debug {"<#{query.id}> Time to process request: #{end_time - start_time}s"}
         | 
| 111 | 
            +
            			
         | 
| 112 | 
            +
            			return response
         | 
| 100 113 | 
             
            		end
         | 
| 101 114 |  | 
| 102 115 | 
             
            		#
         | 
| @@ -113,41 +126,40 @@ module RubyDNS | |
| 113 126 | 
             
            		#   Process::Sys.setuid(server_uid)
         | 
| 114 127 | 
             
            		#   INTERFACES = [socket]
         | 
| 115 128 | 
             
            		#
         | 
| 116 | 
            -
            		def run | 
| 129 | 
            +
            		def run
         | 
| 117 130 | 
             
            			@logger.info "Starting RubyDNS server (v#{RubyDNS::VERSION})..."
         | 
| 118 | 
            -
             | 
| 119 | 
            -
            			interfaces = options[:listen] || DEFAULT_INTERFACES
         | 
| 120 | 
            -
            		
         | 
| 131 | 
            +
            			
         | 
| 121 132 | 
             
            			fire(:setup)
         | 
| 122 | 
            -
             | 
| 133 | 
            +
            			
         | 
| 123 134 | 
             
            			# Setup server sockets
         | 
| 124 | 
            -
            			interfaces.each do |spec|
         | 
| 135 | 
            +
            			@interfaces.each do |spec|
         | 
| 125 136 | 
             
            				if spec.is_a?(BasicSocket)
         | 
| 126 137 | 
             
            					spec.do_not_reverse_lookup
         | 
| 127 | 
            -
            					 | 
| 128 | 
            -
            					protocol = optval.unpack("i")[0]
         | 
| 138 | 
            +
            					protocol = spec.getsockopt(Socket::SOL_SOCKET, Socket::SO_TYPE).unpack("i")[0]
         | 
| 129 139 | 
             
            					ip = spec.local_address.ip_address
         | 
| 130 140 | 
             
            					port = spec.local_address.ip_port
         | 
| 141 | 
            +
            					
         | 
| 131 142 | 
             
            					case protocol
         | 
| 132 143 | 
             
            					when Socket::SOCK_DGRAM
         | 
| 133 | 
            -
            						@logger.info "Attaching to pre-existing UDP socket #{ip}:#{port}"
         | 
| 134 | 
            -
            						 | 
| 144 | 
            +
            						@logger.info "<> Attaching to pre-existing UDP socket #{ip}:#{port}"
         | 
| 145 | 
            +
            						link UDPSocketHandler.new(self, UDPSocketWrapper.new(spec))
         | 
| 135 146 | 
             
            					when Socket::SOCK_STREAM
         | 
| 136 | 
            -
            						@logger.info "Attaching to pre-existing TCP socket #{ip}:#{port}"
         | 
| 137 | 
            -
            						 | 
| 147 | 
            +
            						@logger.info "<> Attaching to pre-existing TCP socket #{ip}:#{port}"
         | 
| 148 | 
            +
            						link TCPSocketHandler.new(self, TCPServerWrapper.new(spec))
         | 
| 138 149 | 
             
            					else
         | 
| 139 | 
            -
            						 | 
| 150 | 
            +
            						raise ArgumentError.new("Unknown socket protocol: #{protocol}")
         | 
| 140 151 | 
             
            					end
         | 
| 152 | 
            +
            				elsif spec[0] == :udp
         | 
| 153 | 
            +
            					@logger.info "<> Listening on #{spec.join(':')}"
         | 
| 154 | 
            +
            					link UDPHandler.new(self, spec[1], spec[2])
         | 
| 155 | 
            +
            				elsif spec[0] == :tcp
         | 
| 156 | 
            +
            					@logger.info "<> Listening on #{spec.join(':')}"
         | 
| 157 | 
            +
            					link TCPHandler.new(self, spec[1], spec[2])
         | 
| 141 158 | 
             
            				else
         | 
| 142 | 
            -
            					 | 
| 143 | 
            -
            					if spec[0] == :udp
         | 
| 144 | 
            -
            						EventMachine.open_datagram_socket(spec[1], spec[2], UDPHandler, self)
         | 
| 145 | 
            -
            					elsif spec[0] == :tcp
         | 
| 146 | 
            -
            						EventMachine.start_server(spec[1], spec[2], TCPHandler, self)
         | 
| 147 | 
            -
            					end
         | 
| 159 | 
            +
            					raise ArgumentError.new("Invalid connection specification: #{spec.inspect}")
         | 
| 148 160 | 
             
            				end
         | 
| 149 161 | 
             
            			end
         | 
| 150 | 
            -
             | 
| 162 | 
            +
            			
         | 
| 151 163 | 
             
            			fire(:start)
         | 
| 152 164 | 
             
            		end
         | 
| 153 165 | 
             
            	end
         | 
| @@ -175,9 +187,9 @@ module RubyDNS | |
| 175 187 | 
             
            			end
         | 
| 176 188 |  | 
| 177 189 | 
             
            			# Invoke the rule, if it matches the incoming request, it is evaluated and returns `true`, otherwise returns `false`.
         | 
| 178 | 
            -
            			def call(server, name, resource_class,  | 
| 190 | 
            +
            			def call(server, name, resource_class, transaction)
         | 
| 179 191 | 
             
            				unless match(name, resource_class)
         | 
| 180 | 
            -
            					server.logger.debug "Resource class #{resource_class} failed to match #{@pattern[1].inspect}!"
         | 
| 192 | 
            +
            					server.logger.debug "<#{transaction.query.id}> Resource class #{resource_class} failed to match #{@pattern[1].inspect}!"
         | 
| 181 193 |  | 
| 182 194 | 
             
            					return false
         | 
| 183 195 | 
             
            				end
         | 
| @@ -188,31 +200,31 @@ module RubyDNS | |
| 188 200 | 
             
            					match_data = @pattern[0].match(name)
         | 
| 189 201 |  | 
| 190 202 | 
             
            					if match_data
         | 
| 191 | 
            -
            						server.logger.debug "Regexp pattern matched with #{match_data.inspect}."
         | 
| 203 | 
            +
            						server.logger.debug "<#{transaction.query.id}> Regexp pattern matched with #{match_data.inspect}."
         | 
| 192 204 |  | 
| 193 | 
            -
            						@callback[ | 
| 205 | 
            +
            						@callback[transaction, match_data]
         | 
| 194 206 |  | 
| 195 207 | 
             
            						return true
         | 
| 196 208 | 
             
            					end
         | 
| 197 209 | 
             
            				when String
         | 
| 198 210 | 
             
            					if @pattern[0] == name
         | 
| 199 | 
            -
            						server.logger.debug "String pattern matched."
         | 
| 211 | 
            +
            						server.logger.debug "<#{transaction.query.id}> String pattern matched."
         | 
| 200 212 |  | 
| 201 | 
            -
            						@callback[ | 
| 213 | 
            +
            						@callback[transaction]
         | 
| 202 214 |  | 
| 203 215 | 
             
            						return true
         | 
| 204 216 | 
             
            					end
         | 
| 205 217 | 
             
            				else
         | 
| 206 218 | 
             
            					if (@pattern[0].call(name, resource_class) rescue false)
         | 
| 207 | 
            -
            						server.logger.debug "Callable pattern matched."
         | 
| 219 | 
            +
            						server.logger.debug "<#{transaction.query.id}> Callable pattern matched."
         | 
| 208 220 |  | 
| 209 | 
            -
            						@callback[ | 
| 221 | 
            +
            						@callback[transaction]
         | 
| 210 222 |  | 
| 211 223 | 
             
            						return true
         | 
| 212 224 | 
             
            					end
         | 
| 213 225 | 
             
            				end
         | 
| 214 226 |  | 
| 215 | 
            -
            				server.logger.debug "No pattern matched."
         | 
| 227 | 
            +
            				server.logger.debug "<#{transaction.query.id}> No pattern matched."
         | 
| 216 228 |  | 
| 217 229 | 
             
            				# We failed to match the pattern.
         | 
| 218 230 | 
             
            				return false
         | 
| @@ -223,6 +235,9 @@ module RubyDNS | |
| 223 235 | 
             
            			end
         | 
| 224 236 | 
             
            		end
         | 
| 225 237 |  | 
| 238 | 
            +
            		# Don't wrap the block going into initialize.
         | 
| 239 | 
            +
            		execute_block_on_receiver :initialize
         | 
| 240 | 
            +
            		
         | 
| 226 241 | 
             
            		# Instantiate a server with a block
         | 
| 227 242 | 
             
            		#
         | 
| 228 243 | 
             
            		#	server = Server.new do
         | 
| @@ -289,32 +304,23 @@ module RubyDNS | |
| 289 304 | 
             
            		end
         | 
| 290 305 |  | 
| 291 306 | 
             
            		# Give a name and a record type, try to match a rule and use it for processing the given arguments.
         | 
| 292 | 
            -
            		def process(name, resource_class,  | 
| 293 | 
            -
            			@logger.debug {"Searching for #{name} #{resource_class.name}"}
         | 
| 307 | 
            +
            		def process(name, resource_class, transaction)
         | 
| 308 | 
            +
            			@logger.debug {"<#{transaction.query.id}> Searching for #{name} #{resource_class.name}"}
         | 
| 294 309 |  | 
| 295 310 | 
             
            			@rules.each do |rule|
         | 
| 296 | 
            -
            				@logger.debug {"Checking rule #{rule}..."}
         | 
| 311 | 
            +
            				@logger.debug {"<#{transaction.query.id}> Checking rule #{rule}..."}
         | 
| 297 312 |  | 
| 298 313 | 
             
            				catch (:next) do
         | 
| 299 314 | 
             
            					# If the rule returns true, we assume that it was successful and no further rules need to be evaluated.
         | 
| 300 | 
            -
            					return if rule.call(self, name, resource_class,  | 
| 315 | 
            +
            					return if rule.call(self, name, resource_class, transaction)
         | 
| 301 316 | 
             
            				end
         | 
| 302 317 | 
             
            			end
         | 
| 303 318 |  | 
| 304 319 | 
             
            			if @otherwise
         | 
| 305 | 
            -
            				@otherwise.call( | 
| 320 | 
            +
            				@otherwise.call(transaction)
         | 
| 306 321 | 
             
            			else
         | 
| 307 | 
            -
            				@logger.warn "Failed to handle #{name} #{resource_class.name}!"
         | 
| 322 | 
            +
            				@logger.warn "<#{transaction.query.id}> Failed to handle #{name} #{resource_class.name}!"
         | 
| 308 323 | 
             
            			end
         | 
| 309 324 | 
             
            		end
         | 
| 310 | 
            -
            		
         | 
| 311 | 
            -
            		# 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.
         | 
| 312 | 
            -
            		def defer(&block)
         | 
| 313 | 
            -
            			fiber = Fiber.current
         | 
| 314 | 
            -
            			
         | 
| 315 | 
            -
            			yield(fiber)
         | 
| 316 | 
            -
            			
         | 
| 317 | 
            -
            			Fiber.yield
         | 
| 318 | 
            -
            		end
         | 
| 319 325 | 
             
            	end
         | 
| 320 326 | 
             
            end
         |