latte 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +8 -0
- data/Rakefile +1 -0
- data/latte.gemspec +24 -0
- data/lib/latte.rb +17 -0
- data/lib/latte/address.rb +36 -0
- data/lib/latte/hex_presenter.rb +7 -0
- data/lib/latte/query.rb +129 -0
- data/lib/latte/response.rb +191 -0
- data/lib/latte/server.rb +81 -0
- data/lib/latte/version.rb +3 -0
- metadata +92 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/latte.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "latte/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "latte"
|
7
|
+
s.version = Latte::VERSION
|
8
|
+
s.authors = ["Craig R Webster"]
|
9
|
+
s.email = ["craig@barkingiguana.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{A DNS framework with configurable query resolver}
|
12
|
+
s.description = %q{Talks DNS and passes queries back to a query resolver build by you that just talks Ruby}
|
13
|
+
|
14
|
+
s.rubyforge_project = "latte"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_runtime_dependency "pethau", ">= 0.0.2"
|
22
|
+
s.add_runtime_dependency "null_logger"
|
23
|
+
s.add_runtime_dependency "bindata"
|
24
|
+
end
|
data/lib/latte.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'null_logger'
|
2
|
+
require 'socket'
|
3
|
+
require 'bindata'
|
4
|
+
require 'pethau'
|
5
|
+
|
6
|
+
class Object
|
7
|
+
include Pethau::InitializeWith
|
8
|
+
include Pethau::DefaultValueOf
|
9
|
+
include Pethau::PrivateAttrAccessor
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'latte/hex_presenter'
|
13
|
+
require 'latte/address'
|
14
|
+
require 'latte/query'
|
15
|
+
require 'latte/response'
|
16
|
+
require 'latte/server'
|
17
|
+
require 'latte/version'
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Latte
|
2
|
+
class Address
|
3
|
+
def self.default
|
4
|
+
@default ||= new
|
5
|
+
end
|
6
|
+
|
7
|
+
initialize_with :raw_string
|
8
|
+
|
9
|
+
def protocol
|
10
|
+
matches = string.scan /^(udp|tcp):\/\//
|
11
|
+
return matches[0][0] unless matches[0].nil?
|
12
|
+
end
|
13
|
+
default_value_of :protocol, 'udp'
|
14
|
+
|
15
|
+
def ip_address
|
16
|
+
matches = string.scan /(\d+\.\d+\.\d+\.\d+)/
|
17
|
+
return matches[0][0] unless matches[0].nil?
|
18
|
+
end
|
19
|
+
default_value_of :ip_address, '127.0.0.1'
|
20
|
+
|
21
|
+
def port
|
22
|
+
matches = string.scan /:(\d+)$/
|
23
|
+
return matches[0][0].to_i unless matches[0].nil?
|
24
|
+
end
|
25
|
+
default_value_of :port, 53
|
26
|
+
|
27
|
+
def string
|
28
|
+
raw_string.to_s.strip
|
29
|
+
end
|
30
|
+
private :string
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
"#{protocol}://#{ip_address}:#{port}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/latte/query.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
module Latte
|
2
|
+
class Query
|
3
|
+
# QTYPE codes:
|
4
|
+
# A 1 a host address
|
5
|
+
# NS 2 an authoritative name server
|
6
|
+
# MD 3 a mail destination (Obsolete - use MX)
|
7
|
+
# MF 4 a mail forwarder (Obsolete - use MX)
|
8
|
+
# CNAME 5 the canonical name for an alias
|
9
|
+
# SOA 6 marks the start of a zone of authority
|
10
|
+
# MB 7 a mailbox domain name (EXPERIMENTAL)
|
11
|
+
# MG 8 a mail group member (EXPERIMENTAL)
|
12
|
+
# MR 9 a mail rename domain name (EXPERIMENTAL)
|
13
|
+
# NULL 10 a null RR (EXPERIMENTAL)
|
14
|
+
# WKS 11 a well known service description
|
15
|
+
# PTR 12 a domain name pointer
|
16
|
+
# HINFO 13 host information
|
17
|
+
# MINFO 14 mailbox or mail list information
|
18
|
+
# MX 15 mail exchange
|
19
|
+
# TXT 16 text strings
|
20
|
+
|
21
|
+
initialize_with :raw_query
|
22
|
+
|
23
|
+
# DNS MESSAGE FORMAT
|
24
|
+
#
|
25
|
+
# Header
|
26
|
+
# Question
|
27
|
+
# Answer
|
28
|
+
# Authority
|
29
|
+
# Additional
|
30
|
+
#
|
31
|
+
# DNS HEADER FORMAT
|
32
|
+
#
|
33
|
+
# OCTET 1,2 ID
|
34
|
+
# OCTET 3,4 QR(1 bit) + OPCODE(4 bit)+ AA(1 bit) + TC(1 bit) +
|
35
|
+
# RD(1 bit)+ RA(1 bit) + Z(3 bit) + RCODE(4 bit)
|
36
|
+
# OCTET 5,6 QDCOUNT
|
37
|
+
# OCTET 7,8 ANCOUNT
|
38
|
+
# OCTET 9,10 NSCOUNT
|
39
|
+
# OCTET 11,12 ARCOUNT
|
40
|
+
#
|
41
|
+
# QUESTION FORMAT
|
42
|
+
#
|
43
|
+
# OCTET 1,2,…n QNAME
|
44
|
+
# OCTET n+1,n+2 QTYPE
|
45
|
+
# OCTET n+3,n+4 QCLASS
|
46
|
+
#
|
47
|
+
# ANSWER, AUTHORITY, ADDITIONAL FORMAT
|
48
|
+
#
|
49
|
+
# OCTET 1,2,..n NAME
|
50
|
+
# OCTET n+1,n+2 TYPE
|
51
|
+
# OCTET n+3,n+4 CLASS
|
52
|
+
# OCTET n+5,n+6,n+7,n+8 TTL
|
53
|
+
# OCTET n+9,n+10 RDLENGTH
|
54
|
+
# OCTET n+11,n+12,….. RDATA
|
55
|
+
class QueryHeader < BinData::Record
|
56
|
+
endian :big
|
57
|
+
uint16 :id
|
58
|
+
bit1 :qr
|
59
|
+
bit4 :opcode
|
60
|
+
bit1 :aa
|
61
|
+
bit1 :tc
|
62
|
+
bit1 :rd
|
63
|
+
bit1 :ra
|
64
|
+
bit3 :z
|
65
|
+
bit4 :rcode
|
66
|
+
uint16 :qdcount
|
67
|
+
uint16 :ancount
|
68
|
+
uint16 :nscount
|
69
|
+
uint16 :arcount
|
70
|
+
end
|
71
|
+
|
72
|
+
class QueryRequest < QueryHeader
|
73
|
+
stringz :qname
|
74
|
+
uint16 :qtype
|
75
|
+
uint16 :qclass
|
76
|
+
end
|
77
|
+
|
78
|
+
def parsed_header
|
79
|
+
@parsed_header ||= build_header
|
80
|
+
end
|
81
|
+
private :parsed_header
|
82
|
+
|
83
|
+
def build_header
|
84
|
+
QueryHeader.read raw_query
|
85
|
+
end
|
86
|
+
private :build_header
|
87
|
+
|
88
|
+
def parsed_record
|
89
|
+
@parsed_record ||= build_record
|
90
|
+
end
|
91
|
+
private :parsed_record
|
92
|
+
|
93
|
+
def build_record
|
94
|
+
case parsed_header.qr.value
|
95
|
+
when 0
|
96
|
+
QueryRequest.read raw_query
|
97
|
+
else
|
98
|
+
raise "Unhandled QR value: #{parsed_header.qr.value}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def method_missing *args
|
103
|
+
result = parsed_record.send *args
|
104
|
+
return result.value if result.respond_to? :value
|
105
|
+
result
|
106
|
+
end
|
107
|
+
|
108
|
+
def header
|
109
|
+
"ID=#{id} QR=#{qr} TC=#{tc} RD=#{rd} RA=#{ra} Z=#{z} " + \
|
110
|
+
"RCODE=#{rcode} QDCOUNT=#{qdcount} ANCOUNT=#{ancount} " + \
|
111
|
+
"NSCOUNT=#{nscount} ARCOUNT=#{arcount}"
|
112
|
+
end
|
113
|
+
|
114
|
+
def human_qname
|
115
|
+
value = parsed_record.qname.value.dup
|
116
|
+
value.gsub! /^[\x00-\x1f]/, ''
|
117
|
+
value.gsub! /[\x00-\x1f]/, '.'
|
118
|
+
value + '.'
|
119
|
+
end
|
120
|
+
|
121
|
+
def query
|
122
|
+
"QNAME=#{human_qname} QTYPE=#{qtype} QCLASS=#{qclass}"
|
123
|
+
end
|
124
|
+
|
125
|
+
def to_s
|
126
|
+
[ header, query ].join " "
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
module Latte
|
2
|
+
class Response
|
3
|
+
initialize_with :query
|
4
|
+
|
5
|
+
class BigEndianRecord < BinData::Record
|
6
|
+
endian :big
|
7
|
+
end
|
8
|
+
|
9
|
+
class ResponseHeader < BigEndianRecord
|
10
|
+
uint16 :id
|
11
|
+
bit1 :qr
|
12
|
+
bit4 :opcode
|
13
|
+
bit1 :aa
|
14
|
+
bit1 :tc
|
15
|
+
bit1 :rd
|
16
|
+
bit1 :ra
|
17
|
+
bit3 :z, :value => 0 # Reserved for future use
|
18
|
+
bit4 :rcode
|
19
|
+
uint16 :qdcount
|
20
|
+
uint16 :ancount
|
21
|
+
uint16 :nscount
|
22
|
+
uint16 :arcount
|
23
|
+
end
|
24
|
+
|
25
|
+
class Question < BigEndianRecord
|
26
|
+
stringz :qname
|
27
|
+
uint16 :qtype
|
28
|
+
uint16 :qclass
|
29
|
+
end
|
30
|
+
|
31
|
+
class Answer < BigEndianRecord
|
32
|
+
stringz :qname
|
33
|
+
uint16 :qtype
|
34
|
+
uint16 :qclass
|
35
|
+
uint32 :ttl
|
36
|
+
uint16 :rdlength, :value => lambda { rdata.length }
|
37
|
+
string :rdata
|
38
|
+
end
|
39
|
+
|
40
|
+
def header
|
41
|
+
ResponseHeader.new.tap { |h|
|
42
|
+
h.id = query.id
|
43
|
+
h.qr = 1 # I'm a response
|
44
|
+
h.opcode = 0 # I'm a standard query
|
45
|
+
h.aa = 0 # I'm not authoritative
|
46
|
+
h.tc = 0 # I wasn't truncated
|
47
|
+
h.rd = 0 # Please don't recursively query
|
48
|
+
h.ra = 0 # Recursion isn't welcome here
|
49
|
+
h.rcode = 0 # There are no errors here
|
50
|
+
h.qdcount = 1 # I'm answering one query
|
51
|
+
h.ancount = answers.size # The number of answer records I'm sending
|
52
|
+
h.nscount = 0 # There are 0 NS records in the authority part
|
53
|
+
h.arcount = 0 # How many additional records am I sending?
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def question
|
58
|
+
Question.new.tap { |q|
|
59
|
+
q.qname = query.qname
|
60
|
+
q.qtype = query.qtype
|
61
|
+
q.qclass = query.qclass
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def answers
|
66
|
+
@answers ||= [ ]
|
67
|
+
end
|
68
|
+
|
69
|
+
class RecordParser
|
70
|
+
initialize_with :record_string
|
71
|
+
private_attr_accessor :record
|
72
|
+
public :record
|
73
|
+
|
74
|
+
# QTYPE codes:
|
75
|
+
# A 1 a host address
|
76
|
+
# NS 2 an authoritative name server
|
77
|
+
# MD 3 a mail destination (Obsolete - use MX)
|
78
|
+
# MF 4 a mail forwarder (Obsolete - use MX)
|
79
|
+
# CNAME 5 the canonical name for an alias
|
80
|
+
# SOA 6 marks the start of a zone of authority
|
81
|
+
# MB 7 a mailbox domain name (EXPERIMENTAL)
|
82
|
+
# MG 8 a mail group member (EXPERIMENTAL)
|
83
|
+
# MR 9 a mail rename domain name (EXPERIMENTAL)
|
84
|
+
# NULL 10 a null RR (EXPERIMENTAL)
|
85
|
+
# WKS 11 a well known service description
|
86
|
+
# PTR 12 a domain name pointer
|
87
|
+
# HINFO 13 host information
|
88
|
+
# MINFO 14 mailbox or mail list information
|
89
|
+
# MX 15 mail exchange
|
90
|
+
# TXT 16 text strings
|
91
|
+
|
92
|
+
def qname
|
93
|
+
parts[0]
|
94
|
+
end
|
95
|
+
|
96
|
+
def qclass
|
97
|
+
parts[1]
|
98
|
+
end
|
99
|
+
|
100
|
+
def encoded_qclass
|
101
|
+
{
|
102
|
+
'IN' => 1,
|
103
|
+
'CH' => 3,
|
104
|
+
'HS' => 4
|
105
|
+
}[qclass]
|
106
|
+
end
|
107
|
+
|
108
|
+
def qtype
|
109
|
+
parts[2]
|
110
|
+
end
|
111
|
+
|
112
|
+
def encoded_qtype
|
113
|
+
{
|
114
|
+
'A' => 1,
|
115
|
+
'NS' => 2,
|
116
|
+
'CNAME' => 5,
|
117
|
+
'SOA' => 6,
|
118
|
+
'PTR' => 12,
|
119
|
+
'HINFO' => 13,
|
120
|
+
'MINFO' => 14,
|
121
|
+
'MX' => 15,
|
122
|
+
'TXT' => 16
|
123
|
+
}[qtype]
|
124
|
+
end
|
125
|
+
|
126
|
+
def ttl
|
127
|
+
parts[3].to_i
|
128
|
+
end
|
129
|
+
|
130
|
+
def rdata
|
131
|
+
parts[4]
|
132
|
+
end
|
133
|
+
|
134
|
+
def encode_name name
|
135
|
+
parts = name.split /\./
|
136
|
+
parts.map! { |p| BinData::Uint8.new(p.length).to_binary_s + p }
|
137
|
+
parts << BinData::Uint8.new(0).to_binary_s
|
138
|
+
parts.join ''
|
139
|
+
end
|
140
|
+
|
141
|
+
def encoded_qname
|
142
|
+
encode_name qname
|
143
|
+
end
|
144
|
+
|
145
|
+
def encoded_rdata
|
146
|
+
# FIXME: Extract this case statment into separate encoders
|
147
|
+
case qtype
|
148
|
+
when 'A', 'PTR'
|
149
|
+
parts = rdata.split /\./
|
150
|
+
parts.map! { |o| BinData::Uint8.new(o.to_i).to_binary_s }
|
151
|
+
parts.join ''
|
152
|
+
when 'NS', 'CNAME'
|
153
|
+
encode_name rdata
|
154
|
+
else
|
155
|
+
raise "I don't know how to encode QTYPE #{qtype.inspect}"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def parts
|
160
|
+
string = record_string.dup
|
161
|
+
string.strip!
|
162
|
+
parts = string.split /\s+/, 5
|
163
|
+
parts.map! { |p| p.strip }
|
164
|
+
parts
|
165
|
+
end
|
166
|
+
|
167
|
+
def execute
|
168
|
+
self.record = Answer.new.tap { |a|
|
169
|
+
a.qname = encoded_qname
|
170
|
+
a.qtype = encoded_qtype
|
171
|
+
a.qclass = encoded_qclass
|
172
|
+
a.ttl = ttl
|
173
|
+
a.rdata = encoded_rdata
|
174
|
+
}
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def add record
|
179
|
+
parser = RecordParser.new record
|
180
|
+
parser.execute
|
181
|
+
record = parser.record
|
182
|
+
answers << record
|
183
|
+
end
|
184
|
+
|
185
|
+
def to_s
|
186
|
+
[ header, question, *answers ].map { |part|
|
187
|
+
part.to_binary_s
|
188
|
+
}.join ''
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
data/lib/latte/server.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
module Latte
|
2
|
+
class Server
|
3
|
+
CHECK_ALIVE_INTERVAL = 5
|
4
|
+
|
5
|
+
initialize_with :resolver, :logger
|
6
|
+
default_value_of :resolver, lambda { |*| }
|
7
|
+
default_value_of :logger do NullLogger.instance end
|
8
|
+
private_attr_accessor :children
|
9
|
+
default_value_of :children, {}
|
10
|
+
private_attr_accessor :addresses
|
11
|
+
default_value_of :addresses do [ Address.default ] end
|
12
|
+
|
13
|
+
def listen_on *addresses
|
14
|
+
return if addresses.empty?
|
15
|
+
self.addresses = addresses
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
logger.debug "I will listen on #{addresses.map(&:to_s).join(',')}"
|
20
|
+
listen
|
21
|
+
end
|
22
|
+
|
23
|
+
def listen
|
24
|
+
loop do
|
25
|
+
addresses.each do |address|
|
26
|
+
next if running_server? address
|
27
|
+
run_server address
|
28
|
+
end
|
29
|
+
sleep CHECK_ALIVE_INTERVAL
|
30
|
+
end
|
31
|
+
end
|
32
|
+
private :listen
|
33
|
+
|
34
|
+
def running_server? address
|
35
|
+
return false unless server_exists_for? address
|
36
|
+
server = server_for address
|
37
|
+
server.alive?
|
38
|
+
end
|
39
|
+
|
40
|
+
def server_for address
|
41
|
+
children[address]
|
42
|
+
end
|
43
|
+
|
44
|
+
def server_exists_for? address
|
45
|
+
!server_for(address).nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
def run_server address
|
49
|
+
if server_exists_for? address
|
50
|
+
logger.warn "Restarting server for #{address}"
|
51
|
+
end
|
52
|
+
logger.debug "Preparing server for #{address}"
|
53
|
+
server_loop = "#{address.protocol}_server_loop"
|
54
|
+
children[address] = Thread.new do
|
55
|
+
begin
|
56
|
+
logger.debug "Server starting on #{address}"
|
57
|
+
Socket.send server_loop, address.ip_address, address.port do |data, client|
|
58
|
+
Thread.new do
|
59
|
+
begin
|
60
|
+
query = Query.new data
|
61
|
+
client_name = client.remote_address.ip_unpack.join ':'
|
62
|
+
logger.debug "#{client_name} > #{address}: #{HexPresenter.new(data)}"
|
63
|
+
response = Response.new query
|
64
|
+
resolver.call query, response
|
65
|
+
logger.debug "#{client_name} < #{address}: #{HexPresenter.new(response)}"
|
66
|
+
client.reply response.to_s
|
67
|
+
rescue => e
|
68
|
+
logger.error [ e.message, e.backtrace ].flatten.join("\n")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
logger.warn "Server loop terminated"
|
73
|
+
rescue => e
|
74
|
+
logger.error [ e.message, e.backtrace ].flatten.join("\n")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
logger.debug "Server started for #{address}"
|
78
|
+
end
|
79
|
+
private :run_server
|
80
|
+
end
|
81
|
+
end
|
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: latte
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Craig R Webster
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-26 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: pethau
|
16
|
+
requirement: &70282543179020 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.0.2
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70282543179020
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: null_logger
|
27
|
+
requirement: &70282539604060 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70282539604060
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: bindata
|
38
|
+
requirement: &70282539603600 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70282539603600
|
47
|
+
description: Talks DNS and passes queries back to a query resolver build by you that
|
48
|
+
just talks Ruby
|
49
|
+
email:
|
50
|
+
- craig@barkingiguana.com
|
51
|
+
executables: []
|
52
|
+
extensions: []
|
53
|
+
extra_rdoc_files: []
|
54
|
+
files:
|
55
|
+
- .gitignore
|
56
|
+
- Gemfile
|
57
|
+
- Gemfile.lock
|
58
|
+
- README.md
|
59
|
+
- Rakefile
|
60
|
+
- latte.gemspec
|
61
|
+
- lib/latte.rb
|
62
|
+
- lib/latte/address.rb
|
63
|
+
- lib/latte/hex_presenter.rb
|
64
|
+
- lib/latte/query.rb
|
65
|
+
- lib/latte/response.rb
|
66
|
+
- lib/latte/server.rb
|
67
|
+
- lib/latte/version.rb
|
68
|
+
homepage: ''
|
69
|
+
licenses: []
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
requirements: []
|
87
|
+
rubyforge_project: latte
|
88
|
+
rubygems_version: 1.8.10
|
89
|
+
signing_key:
|
90
|
+
specification_version: 3
|
91
|
+
summary: A DNS framework with configurable query resolver
|
92
|
+
test_files: []
|