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 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
|
require 'ipaddr'
require_relative 'names'
require_relative 'rr/types'
require_relative 'rr/classes'
%w[a aaaa cname hinfo mr mx ns ptr soa srv txt].each do |file|
require_relative "rr/#{file}"
end
module Net
module DNS
#
# = Net::DNS::RR - DNS Resource Record class
#
# The Net::DNS::RR is the base class for DNS Resource
# Record (RR) objects. A RR is a pack of data that represents
# resources for a DNS zone. The form in which this data is
# shows can be drawed as follow:
#
# "name ttl class type data"
#
# The +name+ is the name of the resource, like an canonical
# name for an +A+ record (internet ip address). The +ttl+ is the
# time to live, expressed in seconds. +type+ and +class+ are
# respectively the type of resource (+A+ for ip addresses, +NS+
# for nameservers, and so on) and the class, which is almost
# always +IN+, the Internet class. At the end, +data+ is the
# value associated to the name for that particular type of
# resource record. An example:
#
# # A record for IP address
# "www.example.com 86400 IN A 172.16.100.1"
#
# # NS record for name server
# "www.example.com 86400 IN NS ns.example.com"
#
# A new RR object can be created in 2 ways: passing a string
# such the ones above, or specifying each field as the pair
# of an hash. See the Net::DNS::RR.new method for details.
#
class RR
include Names
# Base error class.
class Error < StandardError
end
# Error in parsing binary data, maybe from a malformed packet.
class DataError < Error
end
# Regexp matching an RR string
RR_REGEXP = Regexp.new("^\\s*(\\S+)\\s*(\\d+)?\\s+(" +
Net::DNS::RR::Classes.regexp +
"|CLASS\\d+)?\\s*(" +
Net::DNS::RR::Types.regexp +
"|TYPE\\d+)?\\s*(.*)$", Regexp::IGNORECASE)
# Dimension of the sum of class, type, TTL and rdlength fields in a
# RR portion of the packet, in bytes
RRFIXEDSZ = 10
# Create a new instance of Net::DNS::RR class, or an instance of
# any of the subclass of the appropriate type.
#
# Argument can be a string or an hash. With a sting, we can pass
# a RR resource record in the canonical format:
#
# a = Net::DNS::RR.new("foo.example.com. 86400 A 10.1.2.3")
# mx = Net::DNS::RR.new("example.com. 7200 MX 10 mailhost.example.com.")
# cname = Net::DNS::RR.new("www.example.com 300 IN CNAME www1.example.com")
# txt = Net::DNS::RR.new('baz.example.com 3600 HS TXT "text record"')
#
# Incidentally, +a+, +mx+, +cname+ and +txt+ objects will be instances of
# respectively Net::DNS::RR::A, Net::DNS::RR::MX, Net::DNS::RR::CNAME and
# Net::DNS::RR::TXT classes.
#
# The name and RR data are required; all other informations are optional.
# If omitted, the +TTL+ defaults to 10800, +type+ default to +A+ and the RR class
# defaults to +IN+. Omitting the optional fields is useful for creating the
# empty RDATA sections required for certain dynamic update operations.
# All names must be fully qualified. The trailing dot (.) is optional.
#
# The preferred method is however passing an hash with keys and values:
#
# rr = Net::DNS::RR.new(
# :name => "foo.example.com",
# :ttl => 86400,
# :cls => "IN",
# :type => "A",
# :address => "10.1.2.3"
# )
#
# rr = Net::DNS::RR.new(
# :name => "foo.example.com",
# :rdata => "10.1.2.3"
# )
#
# Name and data are required; all the others fields are optionals like
# we've seen before. The data field can be specified either with the
# right name of the resource (+:address+ in the example above) or with
# the generic key +:rdata+. Consult documentation to find the exact name
# for the resource in each subclass.
#
def initialize(arg)
instance = case arg
when String
new_from_string(arg)
when Hash
new_from_hash(arg)
else
raise ArgumentError, "Invalid argument, must be a RR string or an hash of values"
end
if @type.to_s == "ANY"
@cls = Net::DNS::RR::Classes.new("IN")
end
build_pack
set_type
instance
end
# Return a new RR object of the correct type (like Net::DNS::RR::A
# if the type is A) from a binary string, usually obtained from
# network stream.
#
# This method is used when parsing a binary packet by the Packet
# class.
#
def self.parse(data)
o = allocate
obj, offset = o.send(:new_from_binary, data, 0)
obj
end
# Same as RR.parse, but takes an entire packet binary data to
# perform name expansion. Default when analizing a packet
# just received from a network stream.
#
# Return an instance of appropriate class and the offset
# pointing at the end of the data parsed.
#
def self.parse_packet(data, offset)
o = allocate
o.send(:new_from_binary, data, offset)
end
attr_reader :name
attr_reader :ttl
# Type accessor
def type
@type.to_s
end
# Class accessor
def cls
@cls.to_s
end
def value
get_inspect
end
# Data belonging to that appropriate class,
# not to be used (use real accessors instead)
attr_reader :rdata
# Return the RR object in binary data format, suitable
# for using in network streams.
#
# raw_data = rr.data
# puts "RR is #{raw_data.size} bytes long"
#
def data
str = pack_name(@name)
str + [@type.to_i, @cls.to_i, ttl, @rdlength].pack("n2 N n") + get_data
end
# Return the RR object in binary data format, suitable
# for using in network streams, with names compressed.
# Must pass as arguments the offset inside the packet
# and an hash of compressed names.
#
# This method is to be used in other classes and is
# not intended for user space programs.
#
# TO FIX in one of the future releases
#
def comp_data(offset, compnames)
str, offset, names = dn_comp(@name, offset, compnames)
str += [@type.to_i, @cls.to_i, ttl, @rdlength].pack("n2 N n")
offset += Net::DNS::RRFIXEDSZ
[str, offset, names]
end
# Returns a human readable representation of this record.
# The value is always a String.
#
# mx = Net::DNS::RR.new("example.com. 7200 MX 10 mailhost.example.com.")
# #=> example.com. 7200 IN MX 10 mailhost.example.com.
#
def inspect
to_s
end
# Returns a String representation of this record.
#
# mx = Net::DNS::RR.new("example.com. 7200 MX 10 mailhost.example.com.")
# mx.to_s
# #=> "example.com. 7200 IN MX 10 mailhost.example.com."
#
def to_s
items = to_a.map(&:to_s)
if @name.size < 24
items.pack("A24 A8 A8 A8 A*")
else
items.join(" ")
end.to_s
end
# Returns an Array with all the attributes for this record.
#
# mx = Net::DNS::RR.new("example.com. 7200 MX 10 mailhost.example.com.")
# mx.to_a
# #=> ["example.com.", 7200, "IN", "MX", "10 mailhost.example.com."]
#
def to_a
[name, ttl, cls.to_s, type.to_s, value]
end
private
def new_from_string(rrstring)
unless rrstring =~ RR_REGEXP
raise ArgumentError,
"Format error for RR string (maybe CLASS and TYPE not valid?)"
end
# Name of RR - mandatory
begin
@name = Regexp.last_match(1).downcase
rescue NoMethodError
raise ArgumentError, "Missing name field in RR string #{rrstring}"
end
# Time to live for RR, default 3 hours
@ttl = Regexp.last_match(2) ? Regexp.last_match(2).to_i : 10_800
# RR class, default to IN
@cls = Net::DNS::RR::Classes.new Regexp.last_match(3)
# RR type, default to A
@type = Net::DNS::RR::Types.new Regexp.last_match(4)
# All the rest is data
@rdata = Regexp.last_match(5) ? Regexp.last_match(5).strip : ""
if self.class == Net::DNS::RR
Net::DNS::RR.const_get(@type.to_s).new(rrstring)
else
subclass_new_from_string(@rdata)
self.class
end
end
def new_from_hash(args)
# Name field is mandatory
unless args.key? :name
raise ArgumentError, ":name field is mandatory"
end
@name = args[:name].downcase
@ttl = args[:ttl] ? args[:ttl].to_i : 10_800 # Default 3 hours
@type = Net::DNS::RR::Types.new args[:type]
@cls = Net::DNS::RR::Classes.new args[:cls]
@rdata = args[:rdata] ? args[:rdata].strip : ""
@rdlength = args[:rdlength] || @rdata.size
if self.class == Net::DNS::RR
Net::DNS::RR.const_get(@type.to_s).new(args)
else
hash = args.delete_if { |k, _| %i[name ttl type cls].include?(k) }
if hash.key? :rdata
subclass_new_from_string(hash[:rdata])
else
subclass_new_from_hash(hash)
end
self.class
end
end
def new_from_binary(data, offset)
if self.class == Net::DNS::RR
temp = dn_expand(data, offset)[1]
type = Net::DNS::RR::Types.new data.unpack("@#{temp} n")[0]
(eval "Net::DNS::RR::#{type}").parse_packet(data, offset)
else
@name, offset = dn_expand(data, offset)
rrtype, cls, @ttl, @rdlength = data.unpack("@#{offset} n2 N n")
@type = Net::DNS::RR::Types.new rrtype
@cls = Net::DNS::RR::Classes.new cls
offset += RRFIXEDSZ
offset = subclass_new_from_binary(data, offset)
build_pack
set_type
[self, offset]
end
end
# Methods to be overridden by subclasses
def subclass_new_from_array(arr)
end
def subclass_new_from_string(str)
end
def subclass_new_from_hash(hash)
end
def subclass_new_from_binary(data, offset)
end
def build_pack
end
def get_inspect
@rdata
end
def get_data
@rdata
end
def set_type
# TODO: Here we should probably
# raise NotImplementedError
# if we want the method to be implemented in any subclass.
end
def self.new(*args)
o = allocate
obj = o.send(:initialize, *args)
if self == Net::DNS::RR
obj
else
o
end
end
end
end
end
|