coppertone 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +34 -0
  3. data/.travis.yml +7 -0
  4. data/Gemfile +7 -0
  5. data/LICENSE +201 -0
  6. data/README.md +58 -0
  7. data/Rakefile +140 -0
  8. data/coppertone.gemspec +27 -0
  9. data/lib/coppertone/class_builder.rb +20 -0
  10. data/lib/coppertone/directive.rb +38 -0
  11. data/lib/coppertone/dns/error.rb +9 -0
  12. data/lib/coppertone/dns/mock_client.rb +106 -0
  13. data/lib/coppertone/dns/resolv_client.rb +110 -0
  14. data/lib/coppertone/dns.rb +3 -0
  15. data/lib/coppertone/domain_spec.rb +45 -0
  16. data/lib/coppertone/error.rb +29 -0
  17. data/lib/coppertone/ip_address_wrapper.rb +75 -0
  18. data/lib/coppertone/macro_context.rb +67 -0
  19. data/lib/coppertone/macro_string/macro_expand.rb +84 -0
  20. data/lib/coppertone/macro_string/macro_literal.rb +24 -0
  21. data/lib/coppertone/macro_string/macro_parser.rb +62 -0
  22. data/lib/coppertone/macro_string/macro_static_expand.rb +52 -0
  23. data/lib/coppertone/macro_string.rb +31 -0
  24. data/lib/coppertone/mechanism/a.rb +16 -0
  25. data/lib/coppertone/mechanism/all.rb +24 -0
  26. data/lib/coppertone/mechanism/cidr_parser.rb +14 -0
  27. data/lib/coppertone/mechanism/domain_spec_mechanism.rb +18 -0
  28. data/lib/coppertone/mechanism/domain_spec_optional.rb +46 -0
  29. data/lib/coppertone/mechanism/domain_spec_required.rb +37 -0
  30. data/lib/coppertone/mechanism/domain_spec_with_dual_cidr.rb +114 -0
  31. data/lib/coppertone/mechanism/exists.rb +14 -0
  32. data/lib/coppertone/mechanism/include.rb +18 -0
  33. data/lib/coppertone/mechanism/include_matcher.rb +34 -0
  34. data/lib/coppertone/mechanism/ip4.rb +13 -0
  35. data/lib/coppertone/mechanism/ip6.rb +13 -0
  36. data/lib/coppertone/mechanism/ip_mechanism.rb +48 -0
  37. data/lib/coppertone/mechanism/mx.rb +40 -0
  38. data/lib/coppertone/mechanism/ptr.rb +17 -0
  39. data/lib/coppertone/mechanism.rb +32 -0
  40. data/lib/coppertone/modifier/base.rb +24 -0
  41. data/lib/coppertone/modifier/exp.rb +34 -0
  42. data/lib/coppertone/modifier/redirect.rb +17 -0
  43. data/lib/coppertone/modifier/unknown.rb +16 -0
  44. data/lib/coppertone/modifier.rb +30 -0
  45. data/lib/coppertone/qualifier.rb +45 -0
  46. data/lib/coppertone/record.rb +86 -0
  47. data/lib/coppertone/record_evaluator.rb +63 -0
  48. data/lib/coppertone/record_finder.rb +34 -0
  49. data/lib/coppertone/request.rb +68 -0
  50. data/lib/coppertone/request_context.rb +67 -0
  51. data/lib/coppertone/request_count_limiter.rb +36 -0
  52. data/lib/coppertone/result.rb +50 -0
  53. data/lib/coppertone/sender_identity.rb +39 -0
  54. data/lib/coppertone/spf_service.rb +9 -0
  55. data/lib/coppertone/term.rb +13 -0
  56. data/lib/coppertone/utils/domain_utils.rb +59 -0
  57. data/lib/coppertone/utils/host_utils.rb +22 -0
  58. data/lib/coppertone/utils/ip_in_domain_checker.rb +53 -0
  59. data/lib/coppertone/utils/validated_domain_finder.rb +40 -0
  60. data/lib/coppertone/utils.rb +4 -0
  61. data/lib/coppertone/version.rb +3 -0
  62. data/lib/coppertone.rb +48 -0
  63. data/lib/resolv/dns/resource/in/spf.rb +15 -0
  64. data/spec/directive_spec.rb +41 -0
  65. data/spec/dns/resolv_client_spec.rb +307 -0
  66. data/spec/domain_spec_spec.rb +35 -0
  67. data/spec/ip_address_wrapper_spec.rb +67 -0
  68. data/spec/macro_context_spec.rb +69 -0
  69. data/spec/macro_string/macro_expand_spec.rb +79 -0
  70. data/spec/macro_string/macro_literal_spec.rb +27 -0
  71. data/spec/macro_string/macro_static_expand_spec.rb +67 -0
  72. data/spec/macro_string_spec.rb +20 -0
  73. data/spec/mechanism/a_spec.rb +198 -0
  74. data/spec/mechanism/all_spec.rb +22 -0
  75. data/spec/mechanism/exists_spec.rb +91 -0
  76. data/spec/mechanism/include_spec.rb +43 -0
  77. data/spec/mechanism/ip4_spec.rb +110 -0
  78. data/spec/mechanism/ip6_spec.rb +104 -0
  79. data/spec/mechanism/mx_spec.rb +51 -0
  80. data/spec/mechanism/ptr_spec.rb +43 -0
  81. data/spec/mechanism_spec.rb +4 -0
  82. data/spec/modifier_spec.rb +4 -0
  83. data/spec/open_spf/ALL_mechanism_syntax_spec.rb +38 -0
  84. data/spec/open_spf/A_mechanism_syntax_spec.rb +159 -0
  85. data/spec/open_spf/EXISTS_mechanism_syntax_spec.rb +46 -0
  86. data/spec/open_spf/IP4_mechanism_syntax_spec.rb +59 -0
  87. data/spec/open_spf/IP6_mechanism_syntax_spec.rb +60 -0
  88. data/spec/open_spf/Include_mechanism_semantics_and_syntax_spec.rb +56 -0
  89. data/spec/open_spf/Initial_processing_spec.rb +77 -0
  90. data/spec/open_spf/MX_mechanism_syntax_spec.rb +119 -0
  91. data/spec/open_spf/Macro_expansion_rules_spec.rb +154 -0
  92. data/spec/open_spf/PTR_mechanism_syntax_spec.rb +42 -0
  93. data/spec/open_spf/Processing_limits_spec.rb +72 -0
  94. data/spec/open_spf/Record_evaluation_spec.rb +75 -0
  95. data/spec/open_spf/Record_lookup_spec.rb +48 -0
  96. data/spec/open_spf/Selecting_records_spec.rb +61 -0
  97. data/spec/open_spf/Semantics_of_exp_and_other_modifiers_spec.rb +167 -0
  98. data/spec/open_spf/Test_cases_from_implementation_bugs_spec.rb +17 -0
  99. data/spec/qualifier_spec.rb +54 -0
  100. data/spec/record_evaluator_spec.rb +4 -0
  101. data/spec/record_finder_spec.rb +4 -0
  102. data/spec/record_spec.rb +100 -0
  103. data/spec/request_context_spec.rb +43 -0
  104. data/spec/request_count_limiter_spec.rb +28 -0
  105. data/spec/result_spec.rb +4 -0
  106. data/spec/rfc7208-tests.yml +2548 -0
  107. data/spec/sender_identity_spec.rb +69 -0
  108. data/spec/spec_helper.rb +8 -0
  109. data/spec/term_spec.rb +38 -0
  110. data/spec/utils/domain_utils_spec.rb +60 -0
  111. data/spec/utils/host_utils_spec.rb +32 -0
  112. data/spec/utils/ip_in_domain_checker_spec.rb +4 -0
  113. data/spec/utils/validated_domain_finder_spec.rb +4 -0
  114. metadata +306 -0
data/lib/coppertone.rb ADDED
@@ -0,0 +1,48 @@
1
+ require 'active_support/core_ext/object/blank'
2
+
3
+ # A library for evaluating, creating, and analyzing SPF records
4
+ module Coppertone
5
+ class << self
6
+ def config
7
+ @config ||= OpenStruct.new(defaults)
8
+ end
9
+
10
+ def defaults
11
+ {
12
+ hostname: nil,
13
+ message_locale: 'en',
14
+ terms_requiring_dns_lookup_limit: 10,
15
+ dns_lookups_per_mx_mechanism_limit: 10,
16
+ dns_lookups_per_ptr_mechanism_limit: 10,
17
+ void_dns_result_limit: 2,
18
+ dns_client_class: nil,
19
+ default_explanation: 'DEFAULT'
20
+ }
21
+ end
22
+
23
+ # Used for testing.
24
+ def reset_config
25
+ @config = nil
26
+ end
27
+ end
28
+ end
29
+
30
+ require 'coppertone/version'
31
+ require 'coppertone/utils'
32
+ require 'coppertone/error'
33
+ require 'coppertone/request_count_limiter'
34
+ require 'coppertone/sender_identity'
35
+ require 'coppertone/dns'
36
+ require 'coppertone/ip_address_wrapper'
37
+ require 'coppertone/macro_context'
38
+ require 'coppertone/macro_string'
39
+ require 'coppertone/request_context'
40
+ require 'coppertone/domain_spec'
41
+ require 'coppertone/directive'
42
+ require 'coppertone/modifier'
43
+ require 'coppertone/term'
44
+ require 'coppertone/record'
45
+ require 'coppertone/record_evaluator'
46
+ require 'coppertone/record_finder'
47
+ require 'coppertone/request'
48
+ require 'coppertone/spf_service'
@@ -0,0 +1,15 @@
1
+ require 'resolv'
2
+
3
+ class Resolv
4
+ class DNS
5
+ class Resource
6
+ module IN
7
+ # DNS record type for SPF records
8
+ class SPF < Resolv::DNS::Resource::IN::TXT
9
+ # resolv.rb doesn't define an SPF resource type.
10
+ TypeValue = 99 # rubocop:disable Style/ConstantName
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe Coppertone::Directive do
4
+ context '#matching_term' do
5
+ it "returns nil if the term contains a '='" do
6
+ expect(Coppertone::Directive.matching_term('all')).not_to be_nil
7
+ expect(Coppertone::Directive.matching_term('all=')).to be_nil
8
+ expect(Coppertone::Directive.matching_term('all=783')).to be_nil
9
+ end
10
+
11
+ it 'returns nil if the term is not a known directive' do
12
+ expect(Coppertone::Directive.matching_term('unknown')).to be_nil
13
+ expect(Coppertone::Directive.matching_term('~unknown')).to be_nil
14
+ end
15
+
16
+ it 'returns nil if the term is not a known qualifier' do
17
+ expect(Coppertone::Directive.matching_term('+all')).not_to be_nil
18
+ expect(Coppertone::Directive.matching_term('!all')).to be_nil
19
+ end
20
+
21
+ it 'passes attributes' do
22
+ directive = Coppertone::Directive.matching_term('-ip4:192.1.1.1')
23
+ expect(directive).not_to be_nil
24
+ expect(directive.qualifier).to eq(Coppertone::Qualifier::FAIL)
25
+ mechanism = directive.mechanism
26
+ expect(mechanism).not_to be_nil
27
+ expect(mechanism).to eq(Coppertone::Mechanism::IP4.new('192.1.1.1'))
28
+ end
29
+ end
30
+
31
+ context '.evaluate' do
32
+ Coppertone::Qualifier.qualifiers.each do |q|
33
+ it "returns a result with the expected code when it matches #{q.text}" do
34
+ directive = Coppertone::Directive.matching_term("#{q.text}all")
35
+ result = directive.evaluate(nil, nil)
36
+ expect(result).not_to be_nil
37
+ expect(result.code).to eq(q.result_code)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,307 @@
1
+ require 'spec_helper'
2
+
3
+ describe Coppertone::DNS::ResolvClient do
4
+ subject { Coppertone::DNS::ResolvClient.new }
5
+ let(:mock_resolver) { double(:resolver) }
6
+
7
+ context '#fetch_a_records' do
8
+ let(:first_a_addr) { Resolv::IPv4.new([127, 0, 0, 1].pack('CCCC')) }
9
+ let(:first_a_record) { Resolv::DNS::Resource::IN::A.new(first_a_addr) }
10
+ let(:second_a_addr) { Resolv::IPv4.new([192, 168, 8, 14].pack('CCCC')) }
11
+ let(:second_a_record) { Resolv::DNS::Resource::IN::A.new(second_a_addr) }
12
+ let(:record_list) { [first_a_record, second_a_record] }
13
+ let(:domain) { 'example.com' }
14
+ let(:domain_with_trailing) { "#{domain}." }
15
+
16
+ it 'should map the Resolv classes to a set of hashes' do
17
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
18
+ expect(mock_resolver).to receive(:getresources)
19
+ .with(domain, Resolv::DNS::Resource::IN::A).and_return(record_list)
20
+ results = subject.fetch_a_records(domain)
21
+ expect(results.size).to eq(record_list.length)
22
+ expect(results.map { |x| x[:type] })
23
+ .to eq(record_list.length.times.map { 'A' })
24
+ expect(results.map { |x| x[:address] })
25
+ .to eq(record_list.map(&:address).map(&:to_s))
26
+ end
27
+
28
+ it 'should map when the domain has a trailing dot' do
29
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
30
+ expect(mock_resolver).to receive(:getresources)
31
+ .with(domain, Resolv::DNS::Resource::IN::A).and_return(record_list)
32
+ results = subject.fetch_a_records(domain_with_trailing)
33
+ expect(results.size).to eq(record_list.length)
34
+ expect(results.map { |x| x[:type] })
35
+ .to eq(record_list.length.times.map { 'A' })
36
+ expect(results.map { |x| x[:address] })
37
+ .to eq(record_list.map(&:address).map(&:to_s))
38
+ end
39
+
40
+ it 'should map the Resolv errors to Coppertone errors' do
41
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
42
+ expect(mock_resolver).to receive(:getresources)
43
+ .with(domain, Resolv::DNS::Resource::IN::A)
44
+ .and_raise(Resolv::ResolvError)
45
+ expect do
46
+ subject.fetch_a_records(domain_with_trailing)
47
+ end.to raise_error(Coppertone::DNS::Error)
48
+ end
49
+
50
+ it 'should map the Resolv timeout errors to Coppertone errors' do
51
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
52
+ expect(mock_resolver).to receive(:getresources)
53
+ .with(domain, Resolv::DNS::Resource::IN::A)
54
+ .and_raise(Resolv::ResolvTimeout)
55
+ expect do
56
+ subject.fetch_a_records(domain_with_trailing)
57
+ end.to raise_error(Coppertone::DNS::TimeoutError)
58
+ end
59
+ end
60
+
61
+ context '#fetch_aaaa_records' do
62
+ let(:first_aaaa_addr) do
63
+ Resolv::IPv6.create('FE80:10:1:1:202:B3FF:FE1E:8329')
64
+ end
65
+ let(:first_aaaa_record) do
66
+ Resolv::DNS::Resource::IN::AAAA.new(first_aaaa_addr)
67
+ end
68
+ let(:second_aaaa_addr) do
69
+ Resolv::IPv6.create('AB61:10:111:891:4202:B3FF:FE1E:7329')
70
+ end
71
+ let(:second_aaaa_record) do
72
+ Resolv::DNS::Resource::IN::AAAA.new(second_aaaa_addr)
73
+ end
74
+ let(:record_list) { [first_aaaa_record, second_aaaa_record] }
75
+ let(:domain) { 'example.com' }
76
+ let(:domain_with_trailing) { "#{domain}." }
77
+
78
+ it 'should map the Resolv classes to a set of hashes' do
79
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
80
+ expect(mock_resolver).to receive(:getresources)
81
+ .with(domain, Resolv::DNS::Resource::IN::AAAA).and_return(record_list)
82
+ results = subject.fetch_aaaa_records(domain)
83
+ expect(results.size).to eq(record_list.length)
84
+ expect(results.map { |x| x[:type] })
85
+ .to eq(record_list.length.times.map { 'AAAA' })
86
+ expect(results.map { |x| x[:address] })
87
+ .to eq(record_list.map(&:address).map(&:to_s))
88
+ end
89
+
90
+ it 'should map when the domain has a trailing dot' do
91
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
92
+ expect(mock_resolver).to receive(:getresources)
93
+ .with(domain, Resolv::DNS::Resource::IN::AAAA).and_return(record_list)
94
+ results = subject.fetch_aaaa_records(domain_with_trailing)
95
+ expect(results.size).to eq(record_list.length)
96
+ expect(results.map { |x| x[:type] })
97
+ .to eq(record_list.length.times.map { 'AAAA' })
98
+ expect(results.map { |x| x[:address] })
99
+ .to eq(record_list.map(&:address).map(&:to_s))
100
+ end
101
+
102
+ it 'should map the Resolv errors to Coppertone errors' do
103
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
104
+ expect(mock_resolver).to receive(:getresources)
105
+ .with(domain, Resolv::DNS::Resource::IN::AAAA)
106
+ .and_raise(Resolv::ResolvError)
107
+ expect { subject.fetch_aaaa_records(domain_with_trailing) }
108
+ .to raise_error(Coppertone::DNS::Error)
109
+ end
110
+
111
+ it 'should map the Resolv timeout errors to Coppertone errors' do
112
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
113
+ expect(mock_resolver).to receive(:getresources)
114
+ .with(domain, Resolv::DNS::Resource::IN::AAAA)
115
+ .and_raise(Resolv::ResolvTimeout)
116
+ expect { subject.fetch_aaaa_records(domain_with_trailing) }
117
+ .to raise_error(Coppertone::DNS::TimeoutError)
118
+ end
119
+ end
120
+
121
+ context '#fetch_mx_records' do
122
+ let(:first_mx_name) do
123
+ Resolv::DNS::Name.create('alt1.aspmx.l.google.com.')
124
+ end
125
+ let(:first_mx_record) do
126
+ Resolv::DNS::Resource::IN::MX.new(20, first_mx_name)
127
+ end
128
+ let(:second_mx_name) do
129
+ Resolv::DNS::Name.create('aspmx.l.google.com')
130
+ end
131
+ let(:second_mx_record) do
132
+ Resolv::DNS::Resource::IN::MX.new(10, second_mx_name)
133
+ end
134
+ let(:record_list) { [first_mx_record, second_mx_record] }
135
+ let(:domain) { 'example.com' }
136
+ let(:domain_with_trailing) { "#{domain}." }
137
+
138
+ it 'should map the Resolv classes to a set of hashes' do
139
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
140
+ expect(mock_resolver).to receive(:getresources)
141
+ .with(domain, Resolv::DNS::Resource::IN::MX).and_return(record_list)
142
+ results = subject.fetch_mx_records(domain)
143
+ expect(results.size).to eq(record_list.length)
144
+ expect(results.map { |x| x[:type] })
145
+ .to eq(record_list.length.times.map { 'MX' })
146
+ expect(results.map { |x| x[:exchange] })
147
+ .to eq(record_list.map(&:exchange).map(&:to_s))
148
+ end
149
+
150
+ it 'should map when the domain has a trailing dot' do
151
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
152
+ expect(mock_resolver).to receive(:getresources)
153
+ .with(domain, Resolv::DNS::Resource::IN::MX).and_return(record_list)
154
+ results = subject.fetch_mx_records(domain_with_trailing)
155
+ expect(results.size).to eq(record_list.length)
156
+ expect(results.map { |x| x[:type] })
157
+ .to eq(record_list.length.times.map { 'MX' })
158
+ expect(results.map { |x| x[:exchange] })
159
+ .to eq(record_list.map(&:exchange).map(&:to_s))
160
+ end
161
+
162
+ it 'should map the Resolv errors to Coppertone errors' do
163
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
164
+ expect(mock_resolver).to receive(:getresources)
165
+ .with(domain, Resolv::DNS::Resource::IN::MX).and_raise(Resolv::ResolvError)
166
+ expect { subject.fetch_mx_records(domain_with_trailing) }
167
+ .to raise_error(Coppertone::DNS::Error)
168
+ end
169
+
170
+ it 'should map the Resolv timeout errors to Coppertone errors' do
171
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
172
+ expect(mock_resolver).to receive(:getresources)
173
+ .with(domain, Resolv::DNS::Resource::IN::MX)
174
+ .and_raise(Resolv::ResolvTimeout)
175
+ expect { subject.fetch_mx_records(domain_with_trailing) }
176
+ .to raise_error(Coppertone::DNS::TimeoutError)
177
+ end
178
+ end
179
+
180
+ context '#fetch_txt_records' do
181
+ let(:first_txt_string) { SecureRandom.hex(10) }
182
+ let(:first_txt_record) do
183
+ Resolv::DNS::Resource::IN::TXT.new(first_txt_string)
184
+ end
185
+ let(:second_txt_string) { SecureRandom.hex(10) }
186
+ let(:second_txt_string_array) do
187
+ [SecureRandom.hex(10), SecureRandom.hex(10)]
188
+ end
189
+ let(:second_txt_record) do
190
+ Resolv::DNS::Resource::IN::TXT.new(second_txt_string,
191
+ second_txt_string_array)
192
+ end
193
+ let(:record_list) { [first_txt_record, second_txt_record] }
194
+ let(:domain) { 'example.com' }
195
+ let(:domain_with_trailing) { "#{domain}." }
196
+
197
+ it 'should map the Resolv classes to a set of hashes' do
198
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
199
+ expect(mock_resolver).to receive(:getresources)
200
+ .with(domain, Resolv::DNS::Resource::IN::TXT)
201
+ .and_return(record_list)
202
+ results = subject.fetch_txt_records(domain)
203
+ expect(results.size).to eq(record_list.length)
204
+ expect(results.map { |x| x[:type] })
205
+ .to eq(record_list.length.times.map { 'TXT' })
206
+ expect(results.map { |x| x[:text] }).to eq(
207
+ [first_txt_string,
208
+ ([second_txt_string] + second_txt_string_array).join('')])
209
+ end
210
+
211
+ it 'should map when the domain has a trailing dot' do
212
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
213
+ expect(mock_resolver).to receive(:getresources)
214
+ .with(domain, Resolv::DNS::Resource::IN::TXT)
215
+ .and_return(record_list)
216
+ results = subject.fetch_txt_records(domain_with_trailing)
217
+ expect(results.size).to eq(record_list.length)
218
+ expect(results.map { |x| x[:type] })
219
+ .to eq(record_list.length.times.map { 'TXT' })
220
+ expect(results.map { |x| x[:text] }).to eq(
221
+ [first_txt_string,
222
+ ([second_txt_string] + second_txt_string_array).join('')])
223
+ end
224
+
225
+ it 'should map the Resolv errors to Coppertone errors' do
226
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
227
+ expect(mock_resolver).to receive(:getresources)
228
+ .with(domain, Resolv::DNS::Resource::IN::TXT)
229
+ .and_raise(Resolv::ResolvError)
230
+ expect { subject.fetch_txt_records(domain_with_trailing) }
231
+ .to raise_error(Coppertone::DNS::Error)
232
+ end
233
+
234
+ it 'should map the Resolv timeout errors to Coppertone errors' do
235
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
236
+ expect(mock_resolver).to receive(:getresources)
237
+ .with(domain, Resolv::DNS::Resource::IN::TXT)
238
+ .and_raise(Resolv::ResolvTimeout)
239
+ expect { subject.fetch_txt_records(domain_with_trailing) }
240
+ .to raise_error(Coppertone::DNS::TimeoutError)
241
+ end
242
+ end
243
+
244
+ context '#fetch_spf_records' do
245
+ let(:first_spf_string) { SecureRandom.hex(10) }
246
+ let(:first_spf_record) do
247
+ Resolv::DNS::Resource::IN::TXT.new(first_spf_string)
248
+ end
249
+ let(:second_spf_string) { SecureRandom.hex(10) }
250
+ let(:second_spf_string_array) do
251
+ [SecureRandom.hex(10), SecureRandom.hex(10)]
252
+ end
253
+ let(:second_spf_record) do
254
+ Resolv::DNS::Resource::IN::SPF.new(second_spf_string,
255
+ second_spf_string_array)
256
+ end
257
+ let(:record_list) { [first_spf_record, second_spf_record] }
258
+ let(:domain) { 'example.com' }
259
+ let(:domain_with_trailing) { "#{domain}." }
260
+
261
+ it 'should map the Resolv classes to a set of hashes' do
262
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
263
+ expect(mock_resolver).to receive(:getresources)
264
+ .with(domain, Resolv::DNS::Resource::IN::SPF)
265
+ .and_return(record_list)
266
+ results = subject.fetch_spf_records(domain)
267
+ expect(results.size).to eq(record_list.length)
268
+ expect(results.map { |x| x[:type] })
269
+ .to eq(record_list.length.times.map { 'SPF' })
270
+ expect(results.map { |x| x[:text] }).to eq(
271
+ [first_spf_string,
272
+ ([second_spf_string] + second_spf_string_array).join('')])
273
+ end
274
+
275
+ it 'should map when the domain has a trailing dot' do
276
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
277
+ expect(mock_resolver).to receive(:getresources)
278
+ .with(domain, Resolv::DNS::Resource::IN::SPF)
279
+ .and_return(record_list)
280
+ results = subject.fetch_spf_records(domain_with_trailing)
281
+ expect(results.size).to eq(record_list.length)
282
+ expect(results.map { |x| x[:type] })
283
+ .to eq(record_list.length.times.map { 'SPF' })
284
+ expect(results.map { |x| x[:text] }).to eq(
285
+ [first_spf_string,
286
+ ([second_spf_string] + second_spf_string_array).join('')])
287
+ end
288
+
289
+ it 'should map the Resolv errors to Coppertone errors' do
290
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
291
+ expect(mock_resolver).to receive(:getresources)
292
+ .with(domain, Resolv::DNS::Resource::IN::SPF)
293
+ .and_raise(Resolv::ResolvError)
294
+ expect { subject.fetch_spf_records(domain_with_trailing) }
295
+ .to raise_error(Coppertone::DNS::Error)
296
+ end
297
+
298
+ it 'should map the Resolv timeout errors to Coppertone errors' do
299
+ expect(Resolv::DNS).to receive(:new).and_return(mock_resolver)
300
+ expect(mock_resolver).to receive(:getresources)
301
+ .with(domain, Resolv::DNS::Resource::IN::SPF)
302
+ .and_raise(Resolv::ResolvTimeout)
303
+ expect { subject.fetch_spf_records(domain_with_trailing) }
304
+ .to raise_error(Coppertone::DNS::TimeoutError)
305
+ end
306
+ end
307
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe Coppertone::DomainSpec do
4
+ it 'raises no exception for a normal string' do
5
+ expect(Coppertone::DomainSpec.new('gmail.com')).not_to be_nil
6
+ end
7
+
8
+ it 'raises no exception for a macro with allowed terms' do
9
+ expect(Coppertone::DomainSpec.new('%{s}%{l}%{o}%{d}%{i}%{v}'))
10
+ .not_to be_nil
11
+ expect(Coppertone::DomainSpec.new('%{S}%{L}%{O}%{D}%{I}%{V}'))
12
+ .not_to be_nil
13
+ end
14
+
15
+ it 'raises no exception for a macro with reverse' do
16
+ expect(Coppertone::DomainSpec.new('%{sr}%{lr}%{or}%{dr}%{ir}%{vr}'))
17
+ .not_to be_nil
18
+ expect(Coppertone::DomainSpec.new('%{Sr}%{Lr}%{Or}%{Dr}%{Ir}%{Vr}'))
19
+ .not_to be_nil
20
+ end
21
+
22
+ it 'raises an error when the macro string is malformed' do
23
+ expect do
24
+ Coppertone::DomainSpec.new('%&')
25
+ end.to raise_error(Coppertone::DomainSpecParsingError)
26
+ end
27
+
28
+ it 'raises an error when the macro string contains forbidden macros' do
29
+ %w(c r t).each do |m|
30
+ expect do
31
+ Coppertone::DomainSpec.new("%{#{m}}")
32
+ end.to raise_error(Coppertone::DomainSpecParsingError)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+
3
+ describe Coppertone::IPAddressWrapper do
4
+ it 'should raise an ArgumentError when passed a nil arg' do
5
+ expect do
6
+ Coppertone::IPAddressWrapper.new(nil)
7
+ end.to raise_error(ArgumentError)
8
+ end
9
+
10
+ it 'should raise an ArgumentError when passed an invalid arg' do
11
+ expect do
12
+ Coppertone::IPAddressWrapper.new('invalid')
13
+ end.to raise_error(ArgumentError)
14
+ end
15
+
16
+ context 'ipv4' do
17
+ it 'should yield expected values' do
18
+ ip_as_s = '1.2.3.4'
19
+ ipw = Coppertone::IPAddressWrapper.new(ip_as_s)
20
+ expect(ipw.ip_v4).to eq(IPAddr.new(ip_as_s))
21
+ expect(ipw.ip_v6).to be_nil
22
+ expect(ipw.c).to eq(ip_as_s)
23
+ expect(ipw.i).to eq(ip_as_s)
24
+ expect(ipw.v).to eq('in-addr')
25
+ end
26
+
27
+ it 'should raise an ArgumentError when passed an IP with a prefix' do
28
+ expect do
29
+ Coppertone::IPAddressWrapper.new('1.2.3.4/24')
30
+ end.to raise_error(ArgumentError)
31
+ end
32
+ end
33
+
34
+ context 'ipv6' do
35
+ context 'when the address is not IP4 mapped' do
36
+ it 'should yield expected values' do
37
+ ip_as_s = 'FE80:0000:0000:0000:0202:B3FF:FE1E:8329'
38
+ ipw = Coppertone::IPAddressWrapper.new(ip_as_s)
39
+ expect(ipw.ip_v6).to eq(IPAddr.new(ip_as_s))
40
+ expect(ipw.ip_v4).to be_nil
41
+ dotted =
42
+ 'F.E.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.2.0.2.B.3.F.F.F.E.1.E.8.3.2.9'
43
+ expect(ipw.i).to eq(dotted)
44
+ expect(ipw.v).to eq('ip6')
45
+ end
46
+ end
47
+
48
+ context 'when the address is IP4 mapped' do
49
+ it 'should treat it as an IP4 address' do
50
+ ip_v4_as_s = '1.2.3.4'
51
+ ip_as_s = "::ffff:#{ip_v4_as_s}"
52
+ ipw = Coppertone::IPAddressWrapper.new(ip_as_s)
53
+ expect(ipw.ip_v6).to be_nil
54
+ expect(ipw.ip_v4).to eq(IPAddr.new(ip_v4_as_s))
55
+ expect(ipw.c).to eq(ip_v4_as_s)
56
+ expect(ipw.i).to eq(ip_v4_as_s)
57
+ expect(ipw.v).to eq('in-addr')
58
+ end
59
+ end
60
+
61
+ it 'should raise an ArgumentError when passed an IP with a prefix' do
62
+ expect do
63
+ Coppertone::IPAddressWrapper.new('1.2.3.4/96')
64
+ end.to raise_error(ArgumentError)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+
3
+ describe Coppertone::MacroContext do
4
+ let(:domain) { 'yahoo.com' }
5
+ let(:sender) { 'test em!?axt@gmail.com' }
6
+ let(:ip_v4) { '1.2.3.4' }
7
+ let(:ip_v6) { 'fe80::202:b3ff:fe1e:8329' }
8
+
9
+ let(:machine_hostname) { 'mta.receiver-1.xyz.com' }
10
+
11
+ before do
12
+ allow(Coppertone::Utils::HostUtils)
13
+ .to receive(:hostname).and_return(machine_hostname)
14
+ end
15
+
16
+ context 'ip_v4' do
17
+ it 'should map as expected' do
18
+ mc = Coppertone::MacroContext.new(domain, ip_v4, sender)
19
+ expect(mc.s).to eq(sender)
20
+ expect(mc.l).to eq('test em!?axt')
21
+ expect(mc.o).to eq('gmail.com')
22
+ expect(mc.d).to eq(domain)
23
+ expect(mc.i).to eq(ip_v4)
24
+ expect(mc.v).to eq('in-addr')
25
+ # TODO: PMG - Add support for 'p'
26
+ expect(mc.c).to eq(ip_v4)
27
+ before = Time.now.to_i
28
+ t = mc.t
29
+ after = Time.now.to_i
30
+ expect(t >= before).to eq(true)
31
+ expect(t <= after).to eq(true)
32
+ expect(mc.r).to eq(machine_hostname)
33
+
34
+ expect(mc.S).to eq('test%20em!%3Faxt%40gmail.com')
35
+ expect(mc.L).to eq('test%20em!%3Faxt')
36
+ expect(mc.O).to eq('gmail.com')
37
+ expect(mc.D).to eq(domain)
38
+ expect(mc.I).to eq(ip_v4)
39
+ expect(mc.V).to eq('in-addr')
40
+ end
41
+ end
42
+
43
+ context 'ip_v6' do
44
+ it 'should map as expected' do
45
+ mc = Coppertone::MacroContext.new(domain, ip_v6, sender)
46
+ expect(mc.s).to eq(sender)
47
+ expect(mc.l).to eq('test em!?axt')
48
+ expect(mc.o).to eq('gmail.com')
49
+ expect(mc.d).to eq(domain)
50
+ dip = 'F.E.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.2.0.2.B.3.F.F.F.E.1.E.8.3.2.9'
51
+ expect(mc.i).to eq(dip)
52
+ expect(mc.v).to eq('ip6')
53
+ # TODO: PMG - Add support for 'p'
54
+ expect(mc.c).to eq(ip_v6)
55
+ before = Time.now.to_i
56
+ t = mc.t
57
+ after = Time.now.to_i
58
+ expect(t >= before).to eq(true)
59
+ expect(t <= after).to eq(true)
60
+
61
+ expect(mc.S).to eq('test%20em!%3Faxt%40gmail.com')
62
+ expect(mc.L).to eq('test%20em!%3Faxt')
63
+ expect(mc.O).to eq('gmail.com')
64
+ expect(mc.D).to eq(domain)
65
+ expect(mc.I).to eq(dip)
66
+ expect(mc.V).to eq('ip6')
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,79 @@
1
+ require 'spec_helper'
2
+
3
+ describe Coppertone::MacroString::MacroExpand do
4
+ context 'equality' do
5
+ it 'should treat different MacroExpands with the same value as equal' do
6
+ arg = 'dr-'
7
+ macro = Coppertone::MacroString::MacroExpand.new(arg)
8
+ expect(macro == Coppertone::MacroString::MacroExpand.new(arg))
9
+ .to eql(true)
10
+ end
11
+
12
+ it 'should otherwise treat different MacroExpands as unequal' do
13
+ arg = 'dr-'
14
+ macro = Coppertone::MacroString::MacroExpand.new(arg)
15
+ other_arg = 'r3+'
16
+ expect(macro == Coppertone::MacroString::MacroExpand.new(other_arg))
17
+ .to eql(false)
18
+ end
19
+ end
20
+
21
+ context 'expand' do
22
+ let(:domain) { 'yahoo.com' }
23
+ let(:sender) { 'admin-user@gmail.com' }
24
+ let(:ip_v4) { '1.2.3.4' }
25
+ let(:ip_v6) { 'fe80::202:b3ff:fe1e:8329' }
26
+
27
+ context 'ip_v4' do
28
+ let(:context) { Coppertone::MacroContext.new(domain, ip_v4, sender) }
29
+ it 'should expand as expected' do
30
+ expect(Coppertone::MacroString::MacroExpand.new('d').expand(context))
31
+ .to eq('yahoo.com')
32
+ expect(Coppertone::MacroString::MacroExpand.new('d1').expand(context))
33
+ .to eq('com')
34
+ expect(Coppertone::MacroString::MacroExpand.new('dr').expand(context))
35
+ .to eq('com.yahoo')
36
+ expect(Coppertone::MacroString::MacroExpand.new('d1r').expand(context))
37
+ .to eq('yahoo')
38
+
39
+ expect(Coppertone::MacroString::MacroExpand.new('o').expand(context))
40
+ .to eq('gmail.com')
41
+ expect(Coppertone::MacroString::MacroExpand.new('o1').expand(context))
42
+ .to eq('com')
43
+ expect(Coppertone::MacroString::MacroExpand.new('or').expand(context))
44
+ .to eq('com.gmail')
45
+ expect(Coppertone::MacroString::MacroExpand.new('o1r').expand(context))
46
+ .to eq('gmail')
47
+
48
+ expect(Coppertone::MacroString::MacroExpand.new('l').expand(context))
49
+ .to eq('admin-user')
50
+ expect(Coppertone::MacroString::MacroExpand.new('l1').expand(context))
51
+ .to eq('admin-user')
52
+ expect(Coppertone::MacroString::MacroExpand.new('l1-').expand(context))
53
+ .to eq('user')
54
+ expect(Coppertone::MacroString::MacroExpand.new('lr').expand(context))
55
+ .to eq('admin-user')
56
+ expect(Coppertone::MacroString::MacroExpand.new('lr-').expand(context))
57
+ .to eq('user.admin')
58
+ expect(Coppertone::MacroString::MacroExpand.new('l1r-').expand(context))
59
+ .to eq('admin')
60
+
61
+ expect(Coppertone::MacroString::MacroExpand.new('i').expand(context))
62
+ .to eq('1.2.3.4')
63
+ expect(Coppertone::MacroString::MacroExpand.new('i2').expand(context))
64
+ .to eq('3.4')
65
+ expect(Coppertone::MacroString::MacroExpand.new('i6').expand(context))
66
+ .to eq('1.2.3.4')
67
+ expect(Coppertone::MacroString::MacroExpand.new('ir').expand(context))
68
+ .to eq('4.3.2.1')
69
+ expect(Coppertone::MacroString::MacroExpand.new('i2r').expand(context))
70
+ .to eq('2.1')
71
+ expect(Coppertone::MacroString::MacroExpand.new('i6r').expand(context))
72
+ .to eq('4.3.2.1')
73
+
74
+ expect(Coppertone::MacroString::MacroExpand.new('v').expand(context))
75
+ .to eq('in-addr')
76
+ end
77
+ end
78
+ end
79
+ end