dkim-query 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,163 @@
1
+ require 'parslet'
2
+ require 'openssl'
3
+
4
+ module DKIM
5
+ module Query
6
+ #
7
+ # DKIM parser.
8
+ #
9
+ # @see https://tools.ietf.org/html/rfc6376#section-3
10
+ #
11
+ class Parser < Parslet::Parser
12
+
13
+ root :record
14
+ rule(:record) do
15
+ (
16
+ fws? >> key_tag >> fws? >>
17
+ (str(';') >> fws? >> key_tag >> fws?).repeat(0) >> str(';').maybe
18
+ ).as(:tag_list)
19
+ end
20
+
21
+ rule(:key_tag) do
22
+ (
23
+ key_v_tag |
24
+ key_g_tag |
25
+ key_h_tag |
26
+ key_k_tag |
27
+ key_n_tag |
28
+ key_p_tag |
29
+ key_s_tag |
30
+ key_t_tag
31
+ ).as(:tag)
32
+ end
33
+
34
+ def self.key_tag_rule(name,&block)
35
+ rule(:"key_#{name}_tag") do
36
+ str(name).as(:name) >>
37
+ fws? >> str('=') >> fws? >>
38
+ instance_eval(&block).as(:value)
39
+ end
40
+ end
41
+
42
+ def symbol(name)
43
+ str(name).as(:symbol)
44
+ end
45
+
46
+ key_tag_rule('v') { symbol('DKIM1') }
47
+ key_tag_rule('g') { key_g_tag_lpart }
48
+ rule(:key_g_tag_lpart) do
49
+ dot_atom_text.maybe >> (str('*') >> dot_atom_text.maybe).maybe
50
+ end
51
+ rule(:dot_atom_text) { atext.repeat(1) >> (str('.') >> atext.repeat(1)).repeat(0) }
52
+
53
+ key_tag_rule('h') do
54
+ key_h_tag_alg >>
55
+ (fws? >> str(':') >> fws? >> key_h_tag_alg).repeat(0)
56
+ end
57
+ rule(:key_h_tag_alg) { symbol('sha1') | symbol('sha256') | x_key_h_tag_alg }
58
+ rule(:x_key_h_tag_alg) { hyphenated_word }
59
+
60
+ key_tag_rule('k') { key_k_tag_type }
61
+ rule(:key_k_tag_type) { symbol('rsa') | x_key_k_tag_type }
62
+ rule(:x_key_k_tag_type) { hyphenated_word }
63
+
64
+ key_tag_rule('n') { qp_section }
65
+ key_tag_rule('p') { base64string.as(:asn1).maybe }
66
+ key_tag_rule('s') do
67
+ key_s_tag_type >> (fws? >> str(':') >> fws? >> key_s_tag_type).repeat(0)
68
+ end
69
+ rule(:key_s_tag_type) { symbol('email') | symbol('*') | x_key_s_tag_type }
70
+ rule(:x_key_s_tag_type) { hyphenated_word }
71
+
72
+ key_tag_rule('t') do
73
+ key_t_tag_flag >> (fws? >> str(':') >> fws? >> key_t_tag_flag).repeat(0)
74
+ end
75
+ rule(:key_t_tag_flag) { match['ys'] | x_key_t_tag_flag }
76
+ rule(:x_key_t_tag_flag) { hyphenated_word }
77
+
78
+ #
79
+ # Section 2.6: DKIM-Quoted-Printable
80
+ #
81
+ rule(:dkim_quoted_printable) do
82
+ (fws | hex_octet | dkim_safe_char).repeat(0)
83
+ end
84
+ rule(:dkim_safe_char) do
85
+ match['\x21-\x3a'] | str("\x3c") | match['\x3e-\x7e']
86
+ end
87
+
88
+ #
89
+ # Section 2.4: Common ABNF Tokens
90
+ #
91
+ rule(:hypthenated_word) do
92
+ alpha >> ((alpha | digit | str('-')).repeat(0) >> (alpha | digit)).maybe
93
+ end
94
+ rule(:base64string) do
95
+ (alpha | digit | str('+') | str('/') | fws).repeat(1) >>
96
+ (str('=') >> fws? >> (str('=') >> fws?)).maybe
97
+ end
98
+
99
+ #
100
+ # Section 2.3: Whitespace
101
+ #
102
+ rule(:sp) { str(' ') }
103
+ rule(:crlf) { str("\r\n") }
104
+ rule(:wsp) { match['\t '] }
105
+ rule(:wsp?) { wsp.maybe }
106
+ rule(:lwsp) { (wsp | crlf >> wsp).repeat(0) }
107
+ rule(:fws) { (wsp.repeat(0) >> crlf).maybe >> wsp.repeat(1) }
108
+ rule(:fws?) { fws.maybe }
109
+
110
+ #
111
+ # Character rules
112
+ #
113
+ rule(:alpha) { match['a-zA-Z'] }
114
+ rule(:digit) { match['0-9'] }
115
+ rule(:alnum) { match['a-zA-Z0-9'] }
116
+ rule(:valchar) { match['\x21-\x3a'] | match['\x3c-\x7e'] }
117
+ rule(:alnumpunc) { match['a-zA-Z0-9_'] }
118
+ rule(:atext) { alnum | match['!#$%&\'*+\-/=?^ `{|}~'] }
119
+
120
+ #
121
+ # Quoted printable
122
+ #
123
+ rule(:qp_section) do
124
+ (wsp? >> ptext.repeat(1) >> (wsp >> ptext.repeat(1)).repeat(0)).maybe
125
+ end
126
+ rule(:ptext) { hex_octet | safe_char }
127
+ rule(:safe_char) { match['\x21-\x3c'] | match['\x3e-\x7e'] }
128
+ rule(:hex_octet) { str('=') >> match['0-9A-F'].repeat(2,2) }
129
+
130
+ class Transform < Parslet::Transform
131
+
132
+ rule(:symbol => simple(:name)) { name.to_sym }
133
+ # XXX: temporarily disable ASN1 decoding, due to an OpenSSL bug.
134
+ # rule(:asn1 => simple(:blob)) { OpenSSL::ASN1.decode(blob) }
135
+ rule(:asn1 => simple(:blob)) { blob }
136
+
137
+ rule(tag: {name: simple(:name), value: subtree(:value)}) do
138
+ {name.to_sym => value}
139
+ end
140
+
141
+ rule(tag_list: subtree(:hashes)) do
142
+ case hashes
143
+ when Array then hashes.reduce(&:merge!)
144
+ else hashes
145
+ end
146
+ end
147
+
148
+ end
149
+
150
+ #
151
+ # Parses the text into structured data.
152
+ #
153
+ # @param [String] text
154
+ #
155
+ # @return [Hash]
156
+ #
157
+ def self.parse(text)
158
+ Transform.new.apply(new.parse(text))
159
+ end
160
+
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,60 @@
1
+ require 'resolv'
2
+
3
+ module DKIM
4
+ module Query
5
+ #
6
+ # Queries the domain for all DKIM selectors.
7
+ #
8
+ # @param [String] domain
9
+ # The domain to query.
10
+ #
11
+ # @option options [Array<String>] :selectors
12
+ # sub-domain selectors.
13
+ #
14
+ # @option options [Resolv::DNS] :resolver
15
+ # Optional resolver to use.
16
+ #
17
+ # @return [Hash{String => String}]
18
+ # The DKIM keys for the domain.
19
+ #
20
+ # @api semipublic
21
+ #
22
+ def self.query(domain,options={})
23
+ selectors = options.fetch(:selectors) { selectors_for(domain) }
24
+ resolver = options.fetch(:resolver) { Resolv::DNS.new }
25
+
26
+ keys = {}
27
+
28
+ selectors.each do |selector|
29
+ host = "#{selector}._domainkey.#{domain}"
30
+
31
+ begin
32
+ keys[selector] = resolver.getresource(
33
+ host, Resolv::DNS::Resource::IN::TXT
34
+ ).strings.join
35
+ rescue Resolv::ResolvError
36
+ end
37
+ end
38
+
39
+ return keys
40
+ end
41
+
42
+ # Default known selectors
43
+ SELECTORS = %w[default dkim s1024 c1211]
44
+
45
+ #
46
+ # @api private
47
+ #
48
+ def self.selectors_for(host)
49
+ SELECTORS + [host_without_tld(host)]
50
+ end
51
+
52
+ #
53
+ # @api private
54
+ #
55
+ def self.host_without_tld(host)
56
+ host[0,host.rindex('.')]
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,5 @@
1
+ module DKIM
2
+ module Query
3
+ VERSION = '0.2.1'
4
+ end
5
+ end
data/lib/dkim/query.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'dkim/query/version'
2
+ require 'dkim/query/query'
3
+ require 'dkim/query/key'
4
+ require 'dkim/query/domain'
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+ require 'dkim/query/domain'
3
+
4
+ describe Domain do
5
+ let(:domain) { 'yahoo.com' }
6
+ let(:key) do
7
+ Key.new(
8
+ k: :rsa,
9
+ p: "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDrEee0Ri4Juz+QfiWYui/E9UGSXau/2P8LjnTD8V4Unn+2FAZVGE3kL23bzeoULYv4PeleB3gfmJiDJOKU3Ns5L4KJAUUHjFwDebt0NP+sBK0VKeTATL2Yr/S3bT/xhy+1xtj4RkdV7fVxTn56Lb4udUnwuxK4V5b5PdOKj/+XcwIDAQAB",
10
+ n: "A 1024 bit key;",
11
+ )
12
+ end
13
+ let(:selector) { 's1024' }
14
+ let(:keys) { {selector => key} }
15
+
16
+ subject { described_class.new(domain,keys) }
17
+
18
+ describe "#initialize" do
19
+ it "should set name" do
20
+ expect(subject.name).to be domain
21
+ end
22
+
23
+ it "should set keys" do
24
+ expect(subject.keys).to be keys
25
+ end
26
+ end
27
+
28
+ describe ".parse" do
29
+ let(:key) do
30
+ %{k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDrEee0Ri4Juz+QfiWYui/E9UGSXau/2P8LjnTD8V4Unn+2FAZVGE3kL23bzeoULYv4PeleB3gfmJiDJOKU3Ns5L4KJAUUHjFwDebt0NP+sBK0VKeTATL2Yr/S3bT/xhy+1xtj4RkdV7fVxTn56Lb4udUnwuxK4V5b5PdOKj/+XcwIDAQAB; n=A 1024 bit key}
31
+ end
32
+ let(:keys) { {selector => key} }
33
+
34
+ subject { described_class.parse(domain,keys) }
35
+
36
+ it "should parse the keys" do
37
+ expect(subject.keys[selector]).to be_kind_of(Key)
38
+ end
39
+ end
40
+
41
+ describe ".query" do
42
+ subject { described_class.query(domain) }
43
+
44
+ it "should find all known keys" do
45
+ expect(subject.keys).to have_key('s1024')
46
+ end
47
+
48
+ context "with custom selectors" do
49
+ let(:selectors) { ['google', 's1024'] }
50
+
51
+ subject { described_class.query(domain, selectors: selectors) }
52
+
53
+ it "should query those selectors only" do
54
+ expect(subject.keys).to have_key('s1024')
55
+ end
56
+ end
57
+
58
+ context "with no selectors" do
59
+ let(:selectors) { [] }
60
+
61
+ subject { described_class.query(domain, selectors: selectors) }
62
+
63
+ it "should not find any keys" do
64
+ expect(subject.keys).to be_empty
65
+ end
66
+ end
67
+ end
68
+
69
+ describe "#each" do
70
+ context "when given a block" do
71
+ it "should yield each Key" do
72
+ expect { |b| subject.each(&b) }.to yield_with_args(key)
73
+ end
74
+ end
75
+
76
+ context "when not given a block" do
77
+ it "should return an Enumerator" do
78
+ expect(subject.each).to be_kind_of(Enumerator)
79
+ end
80
+ end
81
+ end
82
+
83
+ describe "#[]" do
84
+ context "when given a valid selector" do
85
+ it "should return the key" do
86
+ expect(subject[selector]).to be key
87
+ end
88
+ end
89
+
90
+ context "when given an unknown selector" do
91
+ it "should return nil" do
92
+ expect(subject['foo']).to be_nil
93
+ end
94
+ end
95
+ end
96
+ end
data/spec/key_spec.rb ADDED
@@ -0,0 +1,91 @@
1
+ require 'spec_helper'
2
+ require 'dkim/query/key'
3
+
4
+ describe Key do
5
+ let(:k) { :rsa }
6
+ let(:p) { "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDrEee0Ri4Juz+QfiWYui/E9UGSXau/2P8LjnTD8V4Unn+2FAZVGE3kL23bzeoULYv4PeleB3gfmJiDJOKU3Ns5L4KJAUUHjFwDebt0NP+sBK0VKeTATL2Yr/S3bT/xhy+1xtj4RkdV7fVxTn56Lb4udUnwuxK4V5b5PdOKj/+XcwIDAQAB" }
7
+ let(:n) { "A 1024 bit key;" }
8
+
9
+ subject do
10
+ described_class.new(
11
+ k: k,
12
+ p: p,
13
+ n: n
14
+ )
15
+ end
16
+
17
+ describe "#initialize" do
18
+ it "should set @k" do
19
+ expect(subject.k).to be k
20
+ end
21
+
22
+ it "should set @p" do
23
+ expect(subject.p).to be p
24
+ end
25
+
26
+ it "should set @n" do
27
+ expect(subject.n).to be n
28
+ end
29
+ end
30
+
31
+ let(:record) { %{k=#{k}; p=#{p}; n=#{n}} }
32
+ let(:invalid_record) { "v=spf1" }
33
+
34
+ describe ".parse!" do
35
+ context "when parsing a valid DKIM Key record" do
36
+ subject { described_class.parse!(record) }
37
+
38
+ it "should return a Key" do
39
+ expect(subject).to be_kind_of(described_class)
40
+ end
41
+ end
42
+
43
+ context "when parsing an invalid DKIM Key record" do
44
+ it "should raise an InvalidKey exception" do
45
+ expect {
46
+ described_class.parse!(invalid_record)
47
+ }.to raise_error(InvalidKey)
48
+ end
49
+ end
50
+ end
51
+
52
+ describe ".parse" do
53
+ context "when parsing a valid DKIM Key record" do
54
+ subject { described_class.parse(record) }
55
+
56
+ it "should return a Key" do
57
+ expect(subject).to be_kind_of(described_class)
58
+ end
59
+ end
60
+
61
+ context "when parsing an invalid DKIM Key record" do
62
+ subject { described_class.parse(invalid_record) }
63
+
64
+ it "should return a MalformedKey" do
65
+ expect(subject).to be_kind_of(MalformedKey)
66
+ end
67
+ end
68
+ end
69
+
70
+ describe "#to_hash" do
71
+ subject { super().to_hash }
72
+
73
+ it "should include :k" do
74
+ expect(subject[:k]).to be == k
75
+ end
76
+
77
+ it "should include :p" do
78
+ expect(subject[:p]).to be == p
79
+ end
80
+
81
+ it "should include :n" do
82
+ expect(subject[:n]).to be == n
83
+ end
84
+ end
85
+
86
+ describe "#to_s" do
87
+ it "should return a semicolon deliminited string" do
88
+ expect(subject.to_s).to be == "k=#{k}; p=#{p}; n=#{n}"
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,243 @@
1
+ require 'spec_helper'
2
+ require 'dkim/query/parser'
3
+
4
+ describe Parser do
5
+ describe ".parse" do
6
+ subject { described_class }
7
+
8
+ let(:dkim) do
9
+ %{k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDrEee0Ri4Juz+QfiWYui/E9UGSXau/2P8LjnTD8V4Unn+2FAZVGE3kL23bzeoULYv4PeleB3gfmJiDJOKU3Ns5L4KJAUUHjFwDebt0NP+sBK0VKeTATL2Yr/S3bT/xhy+1xtj4RkdV7fVxTn56Lb4udUnwuxK4V5b5PdOKj/+XcwIDAQAB; n=A 1024 bit key;}
10
+ end
11
+
12
+ it "should parse a DKIM record into a Hash" do
13
+ expect(subject.parse(dkim)).to be == {
14
+ k: :rsa,
15
+ p: %{MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDrEee0Ri4Juz+QfiWYui/E9UGSXau/2P8LjnTD8V4Unn+2FAZVGE3kL23bzeoULYv4PeleB3gfmJiDJOKU3Ns5L4KJAUUHjFwDebt0NP+sBK0VKeTATL2Yr/S3bT/xhy+1xtj4RkdV7fVxTn56Lb4udUnwuxK4V5b5PdOKj/+XcwIDAQAB},
16
+ n: "A 1024 bit key;"
17
+ }
18
+ end
19
+ end
20
+
21
+ describe "tags" do
22
+ describe "v" do
23
+ subject { super().key_v_tag }
24
+
25
+ it "should parse v=DKIM1" do
26
+ expect(subject.parse('v=DKIM1')).to be == {
27
+ name: 'v',
28
+ value: {symbol: 'DKIM1'}
29
+ }
30
+ end
31
+ end
32
+
33
+ describe "g" do
34
+ subject { super().key_g_tag }
35
+ end
36
+
37
+ describe "h" do
38
+ subject { super().key_h_tag }
39
+
40
+ it "should parse h=sha1" do
41
+ expect(subject.parse('h=sha1')).to be == {
42
+ name: 'h',
43
+ value: {symbol: 'sha1'}
44
+ }
45
+ end
46
+
47
+ it "should parse h=sha256" do
48
+ expect(subject.parse('h=sha256')).to be == {
49
+ name: 'h',
50
+ value: {symbol: 'sha256'}
51
+ }
52
+ end
53
+
54
+ it "should parse h=sha1:sha256" do
55
+ expect(subject.parse('h=sha1:sha256')).to be == {
56
+ name: 'h',
57
+ value: [
58
+ {symbol: 'sha1'},
59
+ {symbol: 'sha256'}
60
+ ]
61
+ }
62
+ end
63
+ end
64
+
65
+ describe "k" do
66
+ subject { super().key_k_tag }
67
+
68
+ it "should parse k=rsa" do
69
+ expect(subject.parse('k=rsa')).to be == {
70
+ name: 'k',
71
+ value: {symbol: 'rsa'}
72
+ }
73
+ end
74
+ end
75
+
76
+ describe "n" do
77
+ subject { super().key_n_tag }
78
+
79
+ let(:notes) { %{A 1024 bit key} }
80
+
81
+ it "should parse n=..." do
82
+ expect(subject.parse("n=#{notes}")).to be == {
83
+ name: 'n',
84
+ value: notes
85
+ }
86
+ end
87
+ end
88
+
89
+ describe "p" do
90
+ subject { super().key_p_tag }
91
+
92
+ let(:base64) { %{MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDrEee0Ri4Juz+QfiWYui/E9UGSXau/2P8LjnTD8V4Unn+2FAZVGE3kL23bzeoULYv4PeleB3gfmJiDJOKU3Ns5L4KJAUUHjFwDebt0NP+sBK0VKeTATL2Yr/S3bT/xhy+1xtj4RkdV7fVxTn56Lb4udUnwuxK4V5b5PdOKj/+XcwIDAQAB} }
93
+
94
+ it "should parse p=..." do
95
+ expect(subject.parse("p=#{base64}")).to be == {
96
+ name: 'p',
97
+ value: {asn1: base64}
98
+ }
99
+ end
100
+ end
101
+
102
+ describe "s" do
103
+ subject { super().key_s_tag }
104
+
105
+ it "should parse s=email" do
106
+ expect(subject.parse('s=email')).to be == {
107
+ name: 's',
108
+ value: {symbol: 'email'}
109
+ }
110
+ end
111
+
112
+ it "should parse s=*" do
113
+ expect(subject.parse('s=*')).to be == {
114
+ name: 's',
115
+ value: {symbol: '*'}
116
+ }
117
+ end
118
+
119
+ it "should parse s=email:*" do
120
+ expect(subject.parse('s=email:*')).to be == {
121
+ name: 's',
122
+ value: [{symbol: 'email'}, {symbol: '*'}]
123
+ }
124
+ end
125
+ end
126
+
127
+ describe "t" do
128
+ subject { super().key_t_tag }
129
+
130
+ it "should parse t=y" do
131
+ expect(subject.parse('t=y')).to be == {
132
+ name: 't',
133
+ value: 'y'
134
+ }
135
+ end
136
+
137
+ it "should parse t=s" do
138
+ expect(subject.parse('t=s')).to be == {
139
+ name: 't',
140
+ value: 's'
141
+ }
142
+ end
143
+ end
144
+ end
145
+
146
+ describe "rules" do
147
+ describe "dkim_quoted_printable" do
148
+ end
149
+
150
+ describe "dkim_safe_char" do
151
+ end
152
+
153
+ describe "hyphenated_word" do
154
+ end
155
+
156
+ describe "base64string" do
157
+ end
158
+
159
+ describe "qp_section" do
160
+ subject { super().qp_section }
161
+
162
+ it "should parse \"A\"" do
163
+ expect(subject.parse('A')).to be == 'A'
164
+ end
165
+
166
+ it "should parse \"AAA\"" do
167
+ expect(subject.parse('AAA')).to be == 'AAA'
168
+ end
169
+
170
+ it "should parse \" A\"" do
171
+ expect(subject.parse(' A')).to be == ' A'
172
+ end
173
+
174
+ it "should parse \"A B\"" do
175
+ expect(subject.parse('A B')).to be == 'A B'
176
+ end
177
+
178
+ it "should not parse \" \"" do
179
+ expect {
180
+ subject.parse(' ')
181
+ }.to raise_error(Parslet::ParseFailed)
182
+ end
183
+
184
+ it "should not parse \"A \"" do
185
+ expect {
186
+ subject.parse('A ')
187
+ }.to raise_error(Parslet::ParseFailed)
188
+ end
189
+ end
190
+
191
+ describe "hex_octet" do
192
+ end
193
+ end
194
+
195
+ describe Parser::Transform do
196
+ context "when given {symbol: ...}" do
197
+ let(:string) { 'foo' }
198
+
199
+ it "should convert the string into a Symbol" do
200
+ expect(subject.apply(symbol: string)).to be == string.to_sym
201
+ end
202
+ end
203
+
204
+ context "when given {tag: {name: ..., value: ...}}" do
205
+ let(:name) { 'foo' }
206
+ let(:value) { 'bar' }
207
+
208
+ it "should convert the string into Hash" do
209
+ expect(subject.apply(
210
+ {tag: {name: name, value: value}}
211
+ )).to be == {name.to_sym => value}
212
+ end
213
+ end
214
+
215
+ context "when given {tag: {name: ..., value: [...]}}" do
216
+ let(:name) { 'foo' }
217
+ let(:value) { ['a', 'b'] }
218
+
219
+ it "should convert the string into Hash" do
220
+ expect(subject.apply(
221
+ {tag: {name: name, value: value}}
222
+ )).to be == {name.to_sym => value}
223
+ end
224
+ end
225
+
226
+ context "when {tag_list: {...}}" do
227
+ let(:hash) { {foo: 'bar'} }
228
+
229
+ it "should return the single Hash" do
230
+ expect(subject.apply({tag_list: hash})).to be == hash
231
+ end
232
+ end
233
+
234
+ context "when {tag_list: [{...}, ...]}" do
235
+ let(:hashes) { [{foo: 'bar'}, {baz: 'quix'}] }
236
+ let(:hash) { {foo: 'bar', baz: 'quix'} }
237
+
238
+ it "should merge the Hashes together" do
239
+ expect(subject.apply({tag_list: hashes})).to be == hash
240
+ end
241
+ end
242
+ end
243
+ end