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
@@ -4,10 +4,10 @@ module Coppertone
|
|
4
4
|
class CidrParser
|
5
5
|
def self.parse(raw_length, max_val)
|
6
6
|
return if raw_length.blank?
|
7
|
+
|
7
8
|
length_as_i = raw_length.to_i
|
8
|
-
if length_as_i
|
9
|
-
|
10
|
-
end
|
9
|
+
raise Coppertone::InvalidMechanismError if length_as_i.negative? || length_as_i > max_val
|
10
|
+
|
11
11
|
length_as_i
|
12
12
|
end
|
13
13
|
end
|
@@ -5,14 +5,17 @@ module Coppertone
|
|
5
5
|
attr_reader :domain_spec
|
6
6
|
|
7
7
|
def target_name_from_domain_spec(macro_context, request_context)
|
8
|
-
|
9
|
-
|
8
|
+
if domain_spec
|
9
|
+
domain =
|
10
|
+
domain_spec.expand(macro_context, request_context)
|
11
|
+
end
|
10
12
|
Coppertone::Utils::DomainUtils.macro_expanded_domain(domain)
|
11
13
|
end
|
12
14
|
|
13
15
|
def trim_domain_spec(raw_domain_spec)
|
14
16
|
return nil if raw_domain_spec.blank?
|
15
|
-
|
17
|
+
|
18
|
+
raw_domain_spec[1..]
|
16
19
|
end
|
17
20
|
|
18
21
|
def self.dns_lookup_term?
|
@@ -21,21 +24,25 @@ module Coppertone
|
|
21
24
|
|
22
25
|
def context_dependent?
|
23
26
|
return true unless domain_spec
|
27
|
+
|
24
28
|
domain_spec.context_dependent?
|
25
29
|
end
|
26
30
|
|
27
31
|
def includes_ptr?
|
28
32
|
return false unless domain_spec
|
33
|
+
|
29
34
|
domain_spec.includes_ptr?
|
30
35
|
end
|
31
36
|
|
32
37
|
def target_domain
|
33
38
|
raise Coppertone::NeedsContextError if context_dependent?
|
39
|
+
|
34
40
|
domain_spec.to_s
|
35
41
|
end
|
36
42
|
|
37
43
|
def ==(other)
|
38
44
|
return false unless other.instance_of? self.class
|
45
|
+
|
39
46
|
domain_spec == other.domain_spec
|
40
47
|
end
|
41
48
|
alias eql? ==
|
@@ -10,6 +10,7 @@ module Coppertone
|
|
10
10
|
super(attributes)
|
11
11
|
raw_domain_spec = trim_domain_spec(attributes)
|
12
12
|
raise InvalidMechanismError if raw_domain_spec.blank?
|
13
|
+
|
13
14
|
@domain_spec = Coppertone::DomainSpec.new(raw_domain_spec)
|
14
15
|
rescue Coppertone::MacroStringParsingError
|
15
16
|
raise Coppertone::InvalidMechanismError
|
@@ -14,6 +14,7 @@ module Coppertone
|
|
14
14
|
def initialize(attributes)
|
15
15
|
super(attributes)
|
16
16
|
return if attributes.blank?
|
17
|
+
|
17
18
|
parse_argument(attributes)
|
18
19
|
rescue Coppertone::MacroStringParsingError
|
19
20
|
raise Coppertone::InvalidMechanismError
|
@@ -37,11 +38,13 @@ module Coppertone
|
|
37
38
|
end
|
38
39
|
end
|
39
40
|
|
40
|
-
CIDR_REGEXP = %r{(/(\d*))?(//(\d*))?\z}
|
41
|
+
CIDR_REGEXP = %r{(/(\d*))?(//(\d*))?\z}.freeze
|
41
42
|
def parse_argument(attributes)
|
42
43
|
raise InvalidMechanismError if attributes.blank?
|
44
|
+
|
43
45
|
cidr_matches = CIDR_REGEXP.match(attributes)
|
44
46
|
raise InvalidMechanismError unless cidr_matches
|
47
|
+
|
45
48
|
macro_string, raw_ip_v4_cidr_length, raw_ip_v6_cidr_length =
|
46
49
|
clean_matches(attributes, cidr_matches)
|
47
50
|
process_matches(macro_string, raw_ip_v4_cidr_length,
|
@@ -50,12 +53,15 @@ module Coppertone
|
|
50
53
|
|
51
54
|
def parse_domain_spec(attributes, domain_spec_end)
|
52
55
|
return nil if attributes.blank?
|
56
|
+
|
53
57
|
cand = attributes[0..domain_spec_end]
|
54
58
|
return nil if cand.blank?
|
59
|
+
|
55
60
|
cand = trim_domain_spec(cand)
|
56
61
|
# At this point we've ascertained that there is
|
57
62
|
# a body to the domain spec
|
58
63
|
raise InvalidMechanismError if cand.blank?
|
64
|
+
|
59
65
|
cand
|
60
66
|
end
|
61
67
|
|
@@ -97,6 +103,7 @@ module Coppertone
|
|
97
103
|
|
98
104
|
def ==(other)
|
99
105
|
return false unless other.instance_of? self.class
|
106
|
+
|
100
107
|
domain_spec == other.domain_spec &&
|
101
108
|
ip_v4_cidr_length == other.ip_v4_cidr_length &&
|
102
109
|
ip_v6_cidr_length == other.ip_v6_cidr_length
|
@@ -13,17 +13,20 @@ module Coppertone
|
|
13
13
|
def evaluate_none_result(result, m, r)
|
14
14
|
new_result = super
|
15
15
|
return new_result unless new_result.none?
|
16
|
+
|
16
17
|
raise Coppertone::NoneIncludeResultError
|
17
18
|
end
|
18
19
|
end
|
19
20
|
|
20
21
|
attr_reader :record
|
22
|
+
|
21
23
|
def initialize(record)
|
22
24
|
@record = record
|
23
25
|
end
|
24
26
|
|
25
27
|
def match?(macro_context, request_context)
|
26
28
|
raise Coppertone::NoneIncludeResultError if record.nil?
|
29
|
+
|
27
30
|
record_result =
|
28
31
|
IncludeRecordEvaluator.new(record)
|
29
32
|
.evaluate(macro_context, request_context)
|
@@ -2,7 +2,8 @@ module Coppertone
|
|
2
2
|
class Mechanism
|
3
3
|
# Implements the ip4 mechanism.
|
4
4
|
class IPMechanism < Mechanism
|
5
|
-
attr_reader :netblock
|
5
|
+
attr_reader :netblock, :cidr_length
|
6
|
+
|
6
7
|
def self.create(attributes)
|
7
8
|
new(attributes)
|
8
9
|
end
|
@@ -10,15 +11,16 @@ module Coppertone
|
|
10
11
|
def initialize(attributes)
|
11
12
|
super(attributes)
|
12
13
|
unless attributes.blank?
|
13
|
-
attributes = attributes[1
|
14
|
-
@netblock = parse_netblock(attributes)
|
14
|
+
attributes = attributes[1..] if attributes[0] == ':'
|
15
|
+
@netblock, @cidr_length = parse_netblock(attributes)
|
15
16
|
end
|
16
17
|
raise Coppertone::InvalidMechanismError if @netblock.nil?
|
17
18
|
end
|
18
19
|
|
19
|
-
LEADING_ZEROES_IN_CIDR_REGEXP = %r{
|
20
|
+
LEADING_ZEROES_IN_CIDR_REGEXP = %r{/0\d}.freeze
|
20
21
|
def validate_no_leading_zeroes_in_cidr(ip_as_s)
|
21
|
-
return unless LEADING_ZEROES_IN_CIDR_REGEXP.match(ip_as_s)
|
22
|
+
return unless LEADING_ZEROES_IN_CIDR_REGEXP.match?(ip_as_s)
|
23
|
+
|
22
24
|
raise Coppertone::InvalidMechanismError
|
23
25
|
end
|
24
26
|
|
@@ -31,24 +33,32 @@ module Coppertone
|
|
31
33
|
|
32
34
|
def parse_netblock(ip_as_s)
|
33
35
|
validate_no_leading_zeroes_in_cidr(ip_as_s)
|
34
|
-
addr,
|
35
|
-
return nil if dual
|
36
|
+
addr, cidr_length_as_s, dual = ip_as_s.split('/')
|
37
|
+
return [nil, nil] if dual
|
38
|
+
|
36
39
|
network = IPAddr.new(addr)
|
37
|
-
network = network.mask(
|
38
|
-
network
|
40
|
+
network = network.mask(cidr_length_as_s.to_i) unless cidr_length_as_s.blank?
|
41
|
+
cidr_length = cidr_length_as_s.blank? ? default_cidr(network) : cidr_length_as_s.to_i
|
42
|
+
[network, cidr_length]
|
39
43
|
rescue IP_PARSE_ERROR
|
40
|
-
nil
|
44
|
+
[nil, nil]
|
45
|
+
end
|
46
|
+
|
47
|
+
def default_cidr(network)
|
48
|
+
network.ipv6? ? 128 : 32
|
41
49
|
end
|
42
50
|
|
43
51
|
def match?(macro_context, _request_context)
|
44
52
|
ip = ip_for_match(macro_context)
|
45
53
|
return false unless ip
|
46
54
|
return false unless ip.ipv4? == @netblock.ipv4?
|
55
|
+
|
47
56
|
@netblock.include?(ip)
|
48
57
|
end
|
49
58
|
|
50
59
|
def ==(other)
|
51
60
|
return false unless other.instance_of? self.class
|
61
|
+
|
52
62
|
netblock == other.netblock
|
53
63
|
end
|
54
64
|
end
|
data/lib/coppertone/modifier.rb
CHANGED
@@ -14,10 +14,11 @@ module Coppertone
|
|
14
14
|
class_builder.register(klass.label, klass)
|
15
15
|
end
|
16
16
|
|
17
|
-
MODIFIER_REGEXP = /\A([a-zA-Z]+[a-zA-Z0-9
|
17
|
+
MODIFIER_REGEXP = /\A([a-zA-Z]+[a-zA-Z0-9\-_.]*)=(\S*)\z/.freeze
|
18
18
|
def self.matching_term(text)
|
19
19
|
matches = MODIFIER_REGEXP.match(text)
|
20
20
|
return nil unless matches
|
21
|
+
|
21
22
|
type = matches[1]
|
22
23
|
attributes = matches[2]
|
23
24
|
build(type, attributes) || build_unknown(type, attributes)
|
@@ -28,6 +29,7 @@ module Coppertone
|
|
28
29
|
end
|
29
30
|
|
30
31
|
attr_reader :arguments
|
32
|
+
|
31
33
|
def initialize(arguments)
|
32
34
|
@arguments = arguments
|
33
35
|
end
|
@@ -3,17 +3,21 @@ module Coppertone
|
|
3
3
|
# Base class including logic common to modifiers
|
4
4
|
class Base < Modifier
|
5
5
|
attr_reader :domain_spec
|
6
|
+
|
6
7
|
def initialize(attributes)
|
7
8
|
super(attributes)
|
8
9
|
raise InvalidModifierError if attributes.blank?
|
10
|
+
|
9
11
|
@domain_spec = Coppertone::DomainSpec.new(attributes)
|
10
12
|
rescue Coppertone::MacroStringParsingError
|
11
13
|
raise Coppertone::InvalidModifierError
|
12
14
|
end
|
13
15
|
|
14
16
|
def target_name_from_domain_spec(macro_context, request_context)
|
15
|
-
|
16
|
-
|
17
|
+
if domain_spec
|
18
|
+
domain =
|
19
|
+
domain_spec.expand(macro_context, request_context)
|
20
|
+
end
|
17
21
|
Coppertone::Utils::DomainUtils.macro_expanded_domain(domain)
|
18
22
|
end
|
19
23
|
|
@@ -27,6 +31,7 @@ module Coppertone
|
|
27
31
|
|
28
32
|
def ==(other)
|
29
33
|
return false unless other.instance_of? self.class
|
34
|
+
|
30
35
|
domain_spec == other.domain_spec
|
31
36
|
end
|
32
37
|
end
|
@@ -8,15 +8,18 @@ module Coppertone
|
|
8
8
|
new(attributes)
|
9
9
|
end
|
10
10
|
|
11
|
-
ASCII_REGEXP = /\A[[:ascii:]]*\z
|
11
|
+
ASCII_REGEXP = /\A[[:ascii:]]*\z/.freeze
|
12
12
|
def evaluate(macro_context, request_context)
|
13
13
|
target_name =
|
14
14
|
target_name_from_domain_spec(macro_context, request_context)
|
15
15
|
return nil unless target_name
|
16
|
+
|
16
17
|
macro_string = lookup_macro_string(target_name, request_context)
|
17
18
|
return nil unless macro_string
|
19
|
+
|
18
20
|
expanded = macro_string.expand(macro_context, request_context)
|
19
|
-
return nil unless ASCII_REGEXP.match(expanded)
|
21
|
+
return nil unless ASCII_REGEXP.match?(expanded)
|
22
|
+
|
20
23
|
expanded
|
21
24
|
rescue DNSAdapter::Error
|
22
25
|
nil
|
@@ -27,6 +30,7 @@ module Coppertone
|
|
27
30
|
request_context.dns_client.fetch_txt_records(target_name)
|
28
31
|
return nil if records.empty?
|
29
32
|
return nil if records.size > 1
|
33
|
+
|
30
34
|
MacroString.new(records.first[:text])
|
31
35
|
end
|
32
36
|
|
@@ -3,7 +3,7 @@ module Coppertone
|
|
3
3
|
# do not have contextual dependence.
|
4
4
|
class NullMacroContext
|
5
5
|
RESERVED_REGEXP = Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
|
6
|
-
%w
|
6
|
+
%w[s l o d i p v h c r t].each do |m|
|
7
7
|
define_method(m.upcase) do
|
8
8
|
raise ArgumentError
|
9
9
|
end
|
data/lib/coppertone/qualifier.rb
CHANGED
data/lib/coppertone/record.rb
CHANGED
@@ -3,6 +3,7 @@ module Coppertone
|
|
3
3
|
# record from a text string.
|
4
4
|
class Record
|
5
5
|
attr_reader :text
|
6
|
+
|
6
7
|
def initialize(raw_text)
|
7
8
|
@terms = Coppertone::RecordTermParser.new(raw_text).terms
|
8
9
|
normalize_terms
|
@@ -67,6 +68,7 @@ module Coppertone
|
|
67
68
|
|
68
69
|
def netblocks_only?
|
69
70
|
return false if redirect
|
71
|
+
|
70
72
|
directives.reject(&:all?).reject do |d|
|
71
73
|
d.mechanism.is_a?(Coppertone::Mechanism::IPMechanism)
|
72
74
|
end.empty?
|
@@ -74,7 +76,8 @@ module Coppertone
|
|
74
76
|
|
75
77
|
def context_dependent_evaluation?
|
76
78
|
return true if directives.any?(&:context_dependent?)
|
77
|
-
|
79
|
+
|
80
|
+
redirect&.context_dependent?
|
78
81
|
end
|
79
82
|
|
80
83
|
def exp
|
@@ -82,7 +85,7 @@ module Coppertone
|
|
82
85
|
end
|
83
86
|
|
84
87
|
def context_dependent_explanation?
|
85
|
-
exp
|
88
|
+
exp&.context_dependent?
|
86
89
|
end
|
87
90
|
|
88
91
|
KNOWN_MODS =
|
@@ -95,6 +98,7 @@ module Coppertone
|
|
95
98
|
def find_modifier(klass)
|
96
99
|
arr = modifiers.select { |m| m.is_a?(klass) }
|
97
100
|
raise DuplicateModifierError if arr.size > 1
|
101
|
+
|
98
102
|
arr.first
|
99
103
|
end
|
100
104
|
|
@@ -2,6 +2,7 @@ module Coppertone
|
|
2
2
|
# A helper class for finding SPF records for a domain.
|
3
3
|
class RecordEvaluator
|
4
4
|
attr_reader :record
|
5
|
+
|
5
6
|
def initialize(record)
|
6
7
|
@record = record
|
7
8
|
end
|
@@ -9,6 +10,7 @@ module Coppertone
|
|
9
10
|
def evaluate(macro_context, request_context)
|
10
11
|
result = directive_result(macro_context, request_context)
|
11
12
|
return result unless result.none? || result.fail?
|
13
|
+
|
12
14
|
if result.fail?
|
13
15
|
evaluate_fail_result(result, macro_context, request_context)
|
14
16
|
else
|
@@ -30,6 +32,7 @@ module Coppertone
|
|
30
32
|
def add_exp_to_result(result, macro_context, request_context)
|
31
33
|
result = add_default_exp(result)
|
32
34
|
return result unless record.exp
|
35
|
+
|
33
36
|
computed_exp = record.exp.evaluate(macro_context, request_context)
|
34
37
|
result.explanation = computed_exp if computed_exp
|
35
38
|
result
|
@@ -50,10 +53,12 @@ module Coppertone
|
|
50
53
|
|
51
54
|
def evaluate_none_result(result, macro_context, request_context)
|
52
55
|
return result unless follow_redirect?
|
56
|
+
|
53
57
|
finder =
|
54
58
|
Coppertone::RedirectRecordFinder.new(record.redirect, macro_context,
|
55
59
|
request_context)
|
56
60
|
raise InvalidRedirectError unless finder.target && finder.record
|
61
|
+
|
57
62
|
rc = macro_context.with_domain(finder.target)
|
58
63
|
RecordEvaluator.new(finder.record).evaluate(rc, request_context)
|
59
64
|
end
|
@@ -2,6 +2,7 @@ module Coppertone
|
|
2
2
|
# A helper class for finding SPF records for a domain.
|
3
3
|
class RecordFinder
|
4
4
|
attr_reader :dns_client, :domain
|
5
|
+
|
5
6
|
def initialize(dns_client, domain)
|
6
7
|
@dns_client = dns_client
|
7
8
|
@domain = domain
|
@@ -12,8 +13,7 @@ module Coppertone
|
|
12
13
|
begin
|
13
14
|
validate_txt_records
|
14
15
|
spf_dns_record = txt_records.first
|
15
|
-
|
16
|
-
Record.new(spf_dns_record)
|
16
|
+
spf_dns_record ? Record.new(spf_dns_record) : nil
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
@@ -2,24 +2,28 @@ module Coppertone
|
|
2
2
|
# Parses a record into terms
|
3
3
|
class RecordTermParser
|
4
4
|
VERSION_STR = 'v=spf1'.freeze
|
5
|
-
RECORD_REGEXP = /\A#{VERSION_STR}(\s|\z)/i
|
6
|
-
ALLOWED_CHARACTERS = /\A([\x21-\x7e ]+)\z
|
5
|
+
RECORD_REGEXP = /\A#{VERSION_STR}(\s|\z)/i.freeze
|
6
|
+
ALLOWED_CHARACTERS = /\A([\x21-\x7e ]+)\z/.freeze
|
7
7
|
|
8
8
|
def self.record?(text)
|
9
9
|
return false if text.blank?
|
10
|
-
RECORD_REGEXP.match(text.strip)
|
10
|
+
return true if RECORD_REGEXP.match?(text.strip)
|
11
|
+
|
12
|
+
false
|
11
13
|
end
|
12
14
|
|
13
15
|
attr_reader :text, :terms
|
16
|
+
|
14
17
|
def initialize(text)
|
15
18
|
raise RecordParsingError unless self.class.record?(text)
|
16
|
-
raise RecordParsingError unless ALLOWED_CHARACTERS.match(text)
|
19
|
+
raise RecordParsingError unless ALLOWED_CHARACTERS.match?(text)
|
20
|
+
|
17
21
|
@text = text
|
18
22
|
@terms = Coppertone::TermsParser.new(terms_segment).terms
|
19
23
|
end
|
20
24
|
|
21
25
|
def terms_segment
|
22
|
-
text[VERSION_STR.length
|
26
|
+
text[VERSION_STR.length..].strip
|
23
27
|
end
|
24
28
|
end
|
25
29
|
end
|