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 ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in latte.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # Latte
2
+
3
+ An open DNS platform with an easy to use API.
4
+
5
+
6
+ ## Authors
7
+
8
+ Craig R Webster <http://barkingiguana.com/>
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
@@ -0,0 +1,7 @@
1
+ class HexPresenter
2
+ initialize_with :object
3
+
4
+ def to_s
5
+ object.to_s.unpack('H*')[0].scan(/../).join ' '
6
+ end
7
+ end
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Latte
2
+ VERSION = "0.0.1"
3
+ 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: []