latte 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []