coppertone 0.0.7 → 0.0.12
Sign up to get free protection for your applications and to get access to all the features.
- 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
|