coppertone 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.
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