coppertone 0.0.7 → 0.0.12
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.
- checksums.yaml +5 -5
- data/.circleci/config.yml +79 -0
- data/.codeclimate.yml +28 -0
- data/.github/dependabot.yml +7 -0
- data/.rubocop.yml +47 -6
- data/.travis.yml +2 -6
- data/Gemfile +12 -2
- data/LICENSE +21 -201
- data/Rakefile +12 -6
- data/coppertone.gemspec +9 -9
- data/lib/coppertone/class_builder.rb +2 -0
- data/lib/coppertone/directive.rb +6 -1
- data/lib/coppertone/domain_spec.rb +5 -1
- data/lib/coppertone/error.rb +5 -0
- data/lib/coppertone/ip_address_wrapper.rb +5 -1
- data/lib/coppertone/macro_context.rb +11 -4
- data/lib/coppertone/macro_string.rb +4 -2
- data/lib/coppertone/macro_string/macro_expand.rb +6 -2
- data/lib/coppertone/macro_string/macro_literal.rb +1 -0
- data/lib/coppertone/macro_string/macro_parser.rb +8 -4
- data/lib/coppertone/macro_string/macro_static_expand.rb +3 -1
- data/lib/coppertone/mechanism.rb +2 -0
- data/lib/coppertone/mechanism/all.rb +1 -0
- data/lib/coppertone/mechanism/cidr_parser.rb +3 -3
- data/lib/coppertone/mechanism/domain_spec_mechanism.rb +10 -3
- data/lib/coppertone/mechanism/domain_spec_optional.rb +1 -0
- data/lib/coppertone/mechanism/domain_spec_required.rb +1 -0
- data/lib/coppertone/mechanism/domain_spec_with_dual_cidr.rb +8 -1
- data/lib/coppertone/mechanism/include_matcher.rb +3 -0
- data/lib/coppertone/mechanism/ip_mechanism.rb +20 -10
- data/lib/coppertone/mechanism/mx.rb +1 -0
- data/lib/coppertone/modifier.rb +3 -1
- data/lib/coppertone/modifier/base.rb +7 -2
- data/lib/coppertone/modifier/exp.rb +6 -2
- data/lib/coppertone/modifier/redirect.rb +1 -0
- data/lib/coppertone/modifier/unknown.rb +1 -0
- data/lib/coppertone/null_macro_context.rb +1 -1
- data/lib/coppertone/qualifier.rb +1 -0
- data/lib/coppertone/record.rb +6 -2
- data/lib/coppertone/record_evaluator.rb +5 -0
- data/lib/coppertone/record_finder.rb +2 -2
- data/lib/coppertone/record_term_parser.rb +9 -5
- data/lib/coppertone/redirect_record_finder.rb +3 -1
- data/lib/coppertone/request.rb +26 -23
- data/lib/coppertone/request_context.rb +1 -0
- data/lib/coppertone/request_count_limiter.rb +2 -0
- data/lib/coppertone/result.rb +2 -1
- data/lib/coppertone/sender_identity.rb +2 -1
- data/lib/coppertone/term.rb +1 -0
- data/lib/coppertone/terms_parser.rb +3 -1
- data/lib/coppertone/utils/domain_utils.rb +21 -8
- data/lib/coppertone/utils/ip_in_domain_checker.rb +1 -1
- data/lib/coppertone/utils/validated_domain_finder.rb +2 -1
- data/lib/coppertone/version.rb +1 -1
- data/spec/domain_spec_spec.rb +1 -1
- data/spec/macro_string/macro_static_expand_spec.rb +2 -2
- data/spec/mechanism/ip4_spec.rb +3 -0
- data/spec/mechanism/ip6_spec.rb +3 -0
- data/spec/null_macro_context_spec.rb +1 -1
- data/spec/open_spf/Initial_processing_spec.rb +0 -2
- data/spec/open_spf/Semantics_of_exp_and_other_modifiers_spec.rb +0 -2
- data/spec/record_spec.rb +1 -1
- data/spec/spec_helper.rb +14 -3
- data/spec/term_spec.rb +1 -1
- data/spec/utils/domain_utils_spec.rb +10 -2
- metadata +34 -32
@@ -2,6 +2,7 @@ module Coppertone
|
|
2
2
|
# A helper class for finding SPF records for a redirect modifier.
|
3
3
|
class RedirectRecordFinder
|
4
4
|
attr_reader :redirect, :macro_context, :request_context
|
5
|
+
|
5
6
|
def initialize(redirect, macro_context, request_context)
|
6
7
|
@redirect = redirect
|
7
8
|
@macro_context = macro_context
|
@@ -9,11 +10,12 @@ module Coppertone
|
|
9
10
|
end
|
10
11
|
|
11
12
|
def target
|
12
|
-
@
|
13
|
+
@target ||= redirect.evaluate(macro_context, request_context)
|
13
14
|
end
|
14
15
|
|
15
16
|
def record
|
16
17
|
return unless target
|
18
|
+
|
17
19
|
@record ||= RecordFinder.new(request_context.dns_client, target).record
|
18
20
|
end
|
19
21
|
end
|
data/lib/coppertone/request.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module Coppertone
|
2
2
|
# Represents an SPF request.
|
3
3
|
class Request
|
4
|
-
attr_reader :ip_as_s, :sender, :helo_domain, :options, :result
|
5
|
-
|
4
|
+
attr_reader :ip_as_s, :sender, :helo_domain, :options, :result, :helo_result, :mailfrom_result
|
5
|
+
|
6
6
|
def initialize(ip_as_s, sender, helo_domain, options = {})
|
7
7
|
@ip_as_s = ip_as_s
|
8
8
|
@sender = sender
|
@@ -16,13 +16,15 @@ module Coppertone
|
|
16
16
|
|
17
17
|
def process_helo
|
18
18
|
check_spf_for_helo
|
19
|
-
return nil if helo_result
|
19
|
+
return nil if helo_result&.none?
|
20
|
+
|
20
21
|
helo_result
|
21
22
|
end
|
22
23
|
|
23
24
|
def process_mailfrom
|
24
25
|
check_spf_for_mailfrom
|
25
|
-
return nil if mailfrom_result
|
26
|
+
return nil if mailfrom_result&.none?
|
27
|
+
|
26
28
|
mailfrom_result
|
27
29
|
end
|
28
30
|
|
@@ -34,25 +36,6 @@ module Coppertone
|
|
34
36
|
helo_result.nil? && mailfrom_result.nil?
|
35
37
|
end
|
36
38
|
|
37
|
-
def check_spf_for_helo
|
38
|
-
@helo_result ||= check_spf_for_context(helo_context, 'helo')
|
39
|
-
end
|
40
|
-
|
41
|
-
def check_spf_for_mailfrom
|
42
|
-
@mailfrom_result ||= check_spf_for_context(mailfrom_context, 'mailfrom')
|
43
|
-
end
|
44
|
-
|
45
|
-
def check_spf_for_context(macro_context, identity)
|
46
|
-
record = spf_record(macro_context)
|
47
|
-
@result = spf_request(macro_context, record, identity) if record
|
48
|
-
rescue DNSAdapter::Error => e
|
49
|
-
Result.temperror(e.message)
|
50
|
-
rescue Coppertone::TemperrorError => e
|
51
|
-
Result.temperror(e.message)
|
52
|
-
rescue Coppertone::PermerrorError => e
|
53
|
-
Result.permerror(e.message)
|
54
|
-
end
|
55
|
-
|
56
39
|
def request_context
|
57
40
|
@request_context ||= RequestContext.new(options)
|
58
41
|
end
|
@@ -72,9 +55,29 @@ module Coppertone
|
|
72
55
|
|
73
56
|
def spf_request(macro_context, record, identity)
|
74
57
|
return Result.new(:none) if record.nil?
|
58
|
+
|
75
59
|
r = RecordEvaluator.new(record).evaluate(macro_context, request_context)
|
76
60
|
r.identity = identity
|
77
61
|
r
|
78
62
|
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def check_spf_for_helo
|
67
|
+
@helo_result = check_spf_for_context(helo_context, 'helo')
|
68
|
+
end
|
69
|
+
|
70
|
+
def check_spf_for_mailfrom
|
71
|
+
@mailfrom_result = check_spf_for_context(mailfrom_context, 'mailfrom')
|
72
|
+
end
|
73
|
+
|
74
|
+
def check_spf_for_context(macro_context, identity)
|
75
|
+
record = spf_record(macro_context)
|
76
|
+
@result = spf_request(macro_context, record, identity) if record
|
77
|
+
rescue DNSAdapter::Error, Coppertone::TemperrorError => e
|
78
|
+
Result.temperror(e.message)
|
79
|
+
rescue Coppertone::PermerrorError => e
|
80
|
+
Result.permerror(e.message)
|
81
|
+
end
|
79
82
|
end
|
80
83
|
end
|
@@ -3,6 +3,7 @@ module Coppertone
|
|
3
3
|
# used to track and limit the number of DNS queries of various types.
|
4
4
|
class RequestCountLimiter
|
5
5
|
attr_accessor :count, :limit, :counter_description
|
6
|
+
|
6
7
|
def initialize(limit = nil, counter_description = nil)
|
7
8
|
self.limit = limit
|
8
9
|
self.counter_description = counter_description
|
@@ -26,6 +27,7 @@ module Coppertone
|
|
26
27
|
|
27
28
|
def exceeded?
|
28
29
|
return false unless limited?
|
30
|
+
|
29
31
|
count > limit
|
30
32
|
end
|
31
33
|
|
data/lib/coppertone/result.rb
CHANGED
@@ -16,6 +16,7 @@ module Coppertone
|
|
16
16
|
|
17
17
|
attr_reader :code, :mechanism
|
18
18
|
attr_accessor :explanation, :problem, :identity
|
19
|
+
|
19
20
|
def initialize(code, mechanism = nil)
|
20
21
|
@code = code
|
21
22
|
@mechanism = mechanism
|
@@ -45,7 +46,7 @@ module Coppertone
|
|
45
46
|
Result.new(:neutral)
|
46
47
|
end
|
47
48
|
|
48
|
-
%w
|
49
|
+
%w[none pass fail softfail neutral temperror permerror].each do |t|
|
49
50
|
define_method("#{t}?") do
|
50
51
|
self.class.const_get(t.upcase) == send(:code)
|
51
52
|
end
|
@@ -4,9 +4,10 @@ module Coppertone
|
|
4
4
|
# for the macro letters.
|
5
5
|
class SenderIdentity
|
6
6
|
DEFAULT_LOCALPART = 'postmaster'.freeze
|
7
|
-
EMAIL_ADDRESS_SPLIT_REGEXP = /^(.*)@(.*?)
|
7
|
+
EMAIL_ADDRESS_SPLIT_REGEXP = /^(.*)@(.*?)$/.freeze
|
8
8
|
|
9
9
|
attr_reader :sender, :localpart, :domain
|
10
|
+
|
10
11
|
def initialize(sender)
|
11
12
|
@sender = sender
|
12
13
|
initialize_localpart_and_domain
|
data/lib/coppertone/term.rb
CHANGED
@@ -2,6 +2,7 @@ module Coppertone
|
|
2
2
|
# Parses a un-prefixed string into terms
|
3
3
|
class TermsParser
|
4
4
|
attr_reader :text
|
5
|
+
|
5
6
|
def initialize(text)
|
6
7
|
@text = text
|
7
8
|
end
|
@@ -11,12 +12,13 @@ module Coppertone
|
|
11
12
|
end
|
12
13
|
|
13
14
|
def tokens
|
14
|
-
text.split(/ /).
|
15
|
+
text.split(/ /).reject(&:blank?)
|
15
16
|
end
|
16
17
|
|
17
18
|
def parse_token(token)
|
18
19
|
term = Term.build_from_token(token)
|
19
20
|
raise RecordParsingError unless term
|
21
|
+
|
20
22
|
term
|
21
23
|
end
|
22
24
|
end
|
@@ -7,12 +7,17 @@ module Coppertone
|
|
7
7
|
class DomainUtils
|
8
8
|
def self.valid?(domain)
|
9
9
|
return false if domain.blank?
|
10
|
-
labels = to_ascii_labels(domain)
|
11
|
-
return false if labels.length <= 1
|
12
10
|
return false if domain.length > 253
|
13
|
-
|
11
|
+
|
12
|
+
labels_valid?(to_ascii_labels(domain))
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.labels_valid?(labels)
|
16
|
+
return false if labels.length <= 1
|
17
|
+
return false if labels.first == '*'
|
14
18
|
return false unless valid_tld?(labels.last)
|
15
|
-
|
19
|
+
|
20
|
+
labels.all? { |l| valid_label?(l) }
|
16
21
|
end
|
17
22
|
|
18
23
|
def self.to_labels(domain)
|
@@ -22,6 +27,7 @@ module Coppertone
|
|
22
27
|
def self.parent_domain(domain)
|
23
28
|
labels = to_labels(domain)
|
24
29
|
return '.' if labels.size == 1
|
30
|
+
|
25
31
|
labels.shift
|
26
32
|
labels.join('.')
|
27
33
|
end
|
@@ -34,33 +40,38 @@ module Coppertone
|
|
34
40
|
to_ascii_labels(domain).join('.')
|
35
41
|
end
|
36
42
|
|
37
|
-
NO_DASH_NONNUMERIC_REGEXP = /\A[a-zA-Z0-9]*[a-zA-Z]+[a-zA-Z0-9]*\z
|
38
|
-
NO_DASH_REGEXP = /\A[a-zA-Z0-9]+\z
|
39
|
-
DASH_REGEXP = /\A[a-zA-Z0-9]
|
43
|
+
NO_DASH_NONNUMERIC_REGEXP = /\A[a-zA-Z0-9]*[a-zA-Z]+[a-zA-Z0-9]*\z/.freeze
|
44
|
+
NO_DASH_REGEXP = /\A[a-zA-Z0-9]+\z/.freeze
|
45
|
+
DASH_REGEXP = /\A[a-zA-Z0-9]+-[a-zA-Z0-9\-]*[a-zA-Z0-9]+\z/.freeze
|
40
46
|
|
41
47
|
def self.valid_hostname_label?(l)
|
42
48
|
return false unless valid_label?(l)
|
49
|
+
|
43
50
|
NO_DASH_REGEXP.match(l) || DASH_REGEXP.match(l)
|
44
51
|
end
|
45
52
|
|
46
53
|
def self.valid_tld?(l)
|
47
54
|
return false unless valid_label?(l)
|
55
|
+
|
48
56
|
NO_DASH_NONNUMERIC_REGEXP.match(l) || DASH_REGEXP.match(l)
|
49
57
|
end
|
50
58
|
|
51
59
|
def self.valid_ldh_domain?(domain)
|
52
60
|
return false unless valid?(domain)
|
61
|
+
|
53
62
|
labels = to_ascii_labels(domain)
|
54
63
|
return false unless valid_tld?(labels.last)
|
64
|
+
|
55
65
|
labels.all? { |l| valid_hostname_label?(l) }
|
56
66
|
end
|
57
67
|
|
58
68
|
def self.valid_label?(l)
|
59
|
-
|
69
|
+
!l.empty? && (l.length <= 63) && !l.match(/\s/)
|
60
70
|
end
|
61
71
|
|
62
72
|
def self.macro_expanded_domain(domain)
|
63
73
|
return nil if domain.blank?
|
74
|
+
|
64
75
|
labels = to_ascii_labels(domain)
|
65
76
|
domain = labels.join('.')
|
66
77
|
while domain.length > 253
|
@@ -75,12 +86,14 @@ module Coppertone
|
|
75
86
|
domain_labels = to_ascii_labels(domain)
|
76
87
|
num_labels_in_domain = domain_labels.length
|
77
88
|
return false if subdomain_labels.length <= domain_labels.length
|
89
|
+
|
78
90
|
subdomain_labels.last(num_labels_in_domain) == domain_labels
|
79
91
|
end
|
80
92
|
|
81
93
|
def self.subdomain_or_same?(candidate, domain)
|
82
94
|
return false unless valid?(domain) && valid?(candidate)
|
83
95
|
return true if normalized_domain(domain) == normalized_domain(candidate)
|
96
|
+
|
84
97
|
subdomain_of?(candidate, domain)
|
85
98
|
end
|
86
99
|
end
|
@@ -4,7 +4,8 @@ module Coppertone
|
|
4
4
|
# section 5.5 of the RFC.
|
5
5
|
class ValidatedDomainFinder
|
6
6
|
attr_reader :subdomain_only
|
7
|
-
|
7
|
+
|
8
|
+
def initialize(macro_context, request_context, subdomain_only = true) # rubocop:disable Style/OptionalBooleanParameter
|
8
9
|
@mc = macro_context
|
9
10
|
@request_context = request_context
|
10
11
|
@subdomain_only = subdomain_only
|
data/lib/coppertone/version.rb
CHANGED
data/spec/domain_spec_spec.rb
CHANGED
@@ -26,7 +26,7 @@ describe Coppertone::DomainSpec do
|
|
26
26
|
end
|
27
27
|
|
28
28
|
it 'raises an error when the macro string contains forbidden macros' do
|
29
|
-
%w
|
29
|
+
%w[c r t].each do |m|
|
30
30
|
expect do
|
31
31
|
Coppertone::DomainSpec.new("%{#{m}}")
|
32
32
|
end.to raise_error(Coppertone::DomainSpecParsingError)
|
@@ -2,7 +2,7 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Coppertone::MacroString::MacroStaticExpand do
|
4
4
|
context '#exists_for?' do
|
5
|
-
|
5
|
+
['%%', '%_', '%-'].each do |x|
|
6
6
|
it "should resolve a macro for the key #{x}" do
|
7
7
|
expect(Coppertone::MacroString::MacroStaticExpand.exists_for?(x))
|
8
8
|
.to eq(true)
|
@@ -16,7 +16,7 @@ describe Coppertone::MacroString::MacroStaticExpand do
|
|
16
16
|
end
|
17
17
|
|
18
18
|
context 'macro_for' do
|
19
|
-
|
19
|
+
['%%', '%_', '%-'].each do |x|
|
20
20
|
it "should resolve a macro for the key #{x}" do
|
21
21
|
expect(Coppertone::MacroString::MacroStaticExpand.macro_for(x))
|
22
22
|
.not_to be_nil
|
data/spec/mechanism/ip4_spec.rb
CHANGED
@@ -23,6 +23,7 @@ describe Coppertone::Mechanism::IP4 do
|
|
23
23
|
it 'should not fail if called with an IP v6' do
|
24
24
|
mech = Coppertone::Mechanism::IP4.new(':fe80::202:b3ff:fe1e:8329')
|
25
25
|
expect(mech.netblock).to eq(IPAddr.new('fe80::202:b3ff:fe1e:8329'))
|
26
|
+
expect(mech.cidr_length).to eq(128)
|
26
27
|
expect(mech.to_s).to eq('ip4:fe80::202:b3ff:fe1e:8329')
|
27
28
|
expect(mech).not_to be_includes_ptr
|
28
29
|
expect(mech).not_to be_context_dependent
|
@@ -32,6 +33,7 @@ describe Coppertone::Mechanism::IP4 do
|
|
32
33
|
it 'should work if called with an IP4' do
|
33
34
|
mech = Coppertone::Mechanism::IP4.new(':1.2.3.4')
|
34
35
|
expect(mech.netblock).to eq(IPAddr.new('1.2.3.4'))
|
36
|
+
expect(mech.cidr_length).to eq(32)
|
35
37
|
expect(mech.to_s).to eq('ip4:1.2.3.4')
|
36
38
|
expect(mech).not_to be_includes_ptr
|
37
39
|
expect(mech).not_to be_context_dependent
|
@@ -41,6 +43,7 @@ describe Coppertone::Mechanism::IP4 do
|
|
41
43
|
it 'should work if called with an IP4 with a pfxlen' do
|
42
44
|
mech = Coppertone::Mechanism::IP4.new(':1.2.3.4/4')
|
43
45
|
expect(mech.netblock).to eq(IPAddr.new('1.2.3.4/4'))
|
46
|
+
expect(mech.cidr_length).to eq(4)
|
44
47
|
expect(mech.to_s).to eq('ip4:1.2.3.4/4')
|
45
48
|
expect(mech).not_to be_includes_ptr
|
46
49
|
expect(mech).not_to be_context_dependent
|
data/spec/mechanism/ip6_spec.rb
CHANGED
@@ -23,6 +23,7 @@ describe Coppertone::Mechanism::IP6 do
|
|
23
23
|
it 'should not fail if called with an IP v4' do
|
24
24
|
mech = Coppertone::Mechanism::IP6.new(':1.2.3.4')
|
25
25
|
expect(mech.netblock).to eq(IPAddr.new('1.2.3.4'))
|
26
|
+
expect(mech.cidr_length).to eq(32)
|
26
27
|
expect(mech).not_to be_dns_lookup_term
|
27
28
|
end
|
28
29
|
|
@@ -30,6 +31,7 @@ describe Coppertone::Mechanism::IP6 do
|
|
30
31
|
mech = Coppertone::Mechanism::IP6.new(':fe80::202:b3ff:fe1e:8329')
|
31
32
|
expect(mech.netblock)
|
32
33
|
.to eq(IPAddr.new('fe80::202:b3ff:fe1e:8329'))
|
34
|
+
expect(mech.cidr_length).to eq(128)
|
33
35
|
expect(mech).not_to be_dns_lookup_term
|
34
36
|
end
|
35
37
|
|
@@ -37,6 +39,7 @@ describe Coppertone::Mechanism::IP6 do
|
|
37
39
|
mech = Coppertone::Mechanism::IP6.new(':fe80::202:b3ff:fe1e:8329/64')
|
38
40
|
expect(mech.netblock)
|
39
41
|
.to eq(IPAddr.new('fe80::202:b3ff:fe1e:8329/64'))
|
42
|
+
expect(mech.cidr_length).to eq(64)
|
40
43
|
expect(mech).not_to be_dns_lookup_term
|
41
44
|
end
|
42
45
|
|
data/spec/record_spec.rb
CHANGED
@@ -141,7 +141,7 @@ describe Coppertone::Record do
|
|
141
141
|
.new('v=spf1 include:_spf.domain1.com ip4:1.2.3.4 include:_spf.domain2.com ~all exp=explain._spf.%{d}')
|
142
142
|
includes = r.includes
|
143
143
|
expect(includes.map(&:mechanism).all? { |i| i.is_a?(Coppertone::Mechanism::Include) }).to be_truthy
|
144
|
-
expect(includes.map(&:target_domain)).to eq(%w
|
144
|
+
expect(includes.map(&:target_domain)).to eq(%w[_spf.domain1.com _spf.domain2.com])
|
145
145
|
end
|
146
146
|
end
|
147
147
|
|
data/spec/spec_helper.rb
CHANGED
@@ -1,8 +1,19 @@
|
|
1
1
|
require 'simplecov'
|
2
|
-
|
3
|
-
|
2
|
+
|
3
|
+
if ENV['CI'] == 'true'
|
4
|
+
require 'codecov'
|
5
|
+
SimpleCov.formatter = SimpleCov::Formatter::Codecov
|
6
|
+
end
|
7
|
+
|
8
|
+
# save to CircleCI's artifacts directory if we're on CircleCI
|
9
|
+
if ENV['CIRCLE_ARTIFACTS']
|
10
|
+
dir = File.join(ENV['CIRCLE_ARTIFACTS'], 'coverage')
|
11
|
+
SimpleCov.coverage_dir(dir)
|
12
|
+
end
|
13
|
+
|
14
|
+
SimpleCov.start
|
4
15
|
|
5
16
|
require 'coppertone'
|
6
17
|
|
7
18
|
# Support files
|
8
|
-
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
19
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f }
|