dkim-query 0.2.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.
@@ -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