latte 0.0.1
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.
- 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: []
|