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.
- checksums.yaml +7 -0
- data/.gitignore +34 -0
- data/.travis.yml +7 -0
- data/Gemfile +7 -0
- data/LICENSE +201 -0
- data/README.md +58 -0
- data/Rakefile +140 -0
- data/coppertone.gemspec +27 -0
- data/lib/coppertone/class_builder.rb +20 -0
- data/lib/coppertone/directive.rb +38 -0
- data/lib/coppertone/dns/error.rb +9 -0
- data/lib/coppertone/dns/mock_client.rb +106 -0
- data/lib/coppertone/dns/resolv_client.rb +110 -0
- data/lib/coppertone/dns.rb +3 -0
- data/lib/coppertone/domain_spec.rb +45 -0
- data/lib/coppertone/error.rb +29 -0
- data/lib/coppertone/ip_address_wrapper.rb +75 -0
- data/lib/coppertone/macro_context.rb +67 -0
- data/lib/coppertone/macro_string/macro_expand.rb +84 -0
- data/lib/coppertone/macro_string/macro_literal.rb +24 -0
- data/lib/coppertone/macro_string/macro_parser.rb +62 -0
- data/lib/coppertone/macro_string/macro_static_expand.rb +52 -0
- data/lib/coppertone/macro_string.rb +31 -0
- data/lib/coppertone/mechanism/a.rb +16 -0
- data/lib/coppertone/mechanism/all.rb +24 -0
- data/lib/coppertone/mechanism/cidr_parser.rb +14 -0
- data/lib/coppertone/mechanism/domain_spec_mechanism.rb +18 -0
- data/lib/coppertone/mechanism/domain_spec_optional.rb +46 -0
- data/lib/coppertone/mechanism/domain_spec_required.rb +37 -0
- data/lib/coppertone/mechanism/domain_spec_with_dual_cidr.rb +114 -0
- data/lib/coppertone/mechanism/exists.rb +14 -0
- data/lib/coppertone/mechanism/include.rb +18 -0
- data/lib/coppertone/mechanism/include_matcher.rb +34 -0
- data/lib/coppertone/mechanism/ip4.rb +13 -0
- data/lib/coppertone/mechanism/ip6.rb +13 -0
- data/lib/coppertone/mechanism/ip_mechanism.rb +48 -0
- data/lib/coppertone/mechanism/mx.rb +40 -0
- data/lib/coppertone/mechanism/ptr.rb +17 -0
- data/lib/coppertone/mechanism.rb +32 -0
- data/lib/coppertone/modifier/base.rb +24 -0
- data/lib/coppertone/modifier/exp.rb +34 -0
- data/lib/coppertone/modifier/redirect.rb +17 -0
- data/lib/coppertone/modifier/unknown.rb +16 -0
- data/lib/coppertone/modifier.rb +30 -0
- data/lib/coppertone/qualifier.rb +45 -0
- data/lib/coppertone/record.rb +86 -0
- data/lib/coppertone/record_evaluator.rb +63 -0
- data/lib/coppertone/record_finder.rb +34 -0
- data/lib/coppertone/request.rb +68 -0
- data/lib/coppertone/request_context.rb +67 -0
- data/lib/coppertone/request_count_limiter.rb +36 -0
- data/lib/coppertone/result.rb +50 -0
- data/lib/coppertone/sender_identity.rb +39 -0
- data/lib/coppertone/spf_service.rb +9 -0
- data/lib/coppertone/term.rb +13 -0
- data/lib/coppertone/utils/domain_utils.rb +59 -0
- data/lib/coppertone/utils/host_utils.rb +22 -0
- data/lib/coppertone/utils/ip_in_domain_checker.rb +53 -0
- data/lib/coppertone/utils/validated_domain_finder.rb +40 -0
- data/lib/coppertone/utils.rb +4 -0
- data/lib/coppertone/version.rb +3 -0
- data/lib/coppertone.rb +48 -0
- data/lib/resolv/dns/resource/in/spf.rb +15 -0
- data/spec/directive_spec.rb +41 -0
- data/spec/dns/resolv_client_spec.rb +307 -0
- data/spec/domain_spec_spec.rb +35 -0
- data/spec/ip_address_wrapper_spec.rb +67 -0
- data/spec/macro_context_spec.rb +69 -0
- data/spec/macro_string/macro_expand_spec.rb +79 -0
- data/spec/macro_string/macro_literal_spec.rb +27 -0
- data/spec/macro_string/macro_static_expand_spec.rb +67 -0
- data/spec/macro_string_spec.rb +20 -0
- data/spec/mechanism/a_spec.rb +198 -0
- data/spec/mechanism/all_spec.rb +22 -0
- data/spec/mechanism/exists_spec.rb +91 -0
- data/spec/mechanism/include_spec.rb +43 -0
- data/spec/mechanism/ip4_spec.rb +110 -0
- data/spec/mechanism/ip6_spec.rb +104 -0
- data/spec/mechanism/mx_spec.rb +51 -0
- data/spec/mechanism/ptr_spec.rb +43 -0
- data/spec/mechanism_spec.rb +4 -0
- data/spec/modifier_spec.rb +4 -0
- data/spec/open_spf/ALL_mechanism_syntax_spec.rb +38 -0
- data/spec/open_spf/A_mechanism_syntax_spec.rb +159 -0
- data/spec/open_spf/EXISTS_mechanism_syntax_spec.rb +46 -0
- data/spec/open_spf/IP4_mechanism_syntax_spec.rb +59 -0
- data/spec/open_spf/IP6_mechanism_syntax_spec.rb +60 -0
- data/spec/open_spf/Include_mechanism_semantics_and_syntax_spec.rb +56 -0
- data/spec/open_spf/Initial_processing_spec.rb +77 -0
- data/spec/open_spf/MX_mechanism_syntax_spec.rb +119 -0
- data/spec/open_spf/Macro_expansion_rules_spec.rb +154 -0
- data/spec/open_spf/PTR_mechanism_syntax_spec.rb +42 -0
- data/spec/open_spf/Processing_limits_spec.rb +72 -0
- data/spec/open_spf/Record_evaluation_spec.rb +75 -0
- data/spec/open_spf/Record_lookup_spec.rb +48 -0
- data/spec/open_spf/Selecting_records_spec.rb +61 -0
- data/spec/open_spf/Semantics_of_exp_and_other_modifiers_spec.rb +167 -0
- data/spec/open_spf/Test_cases_from_implementation_bugs_spec.rb +17 -0
- data/spec/qualifier_spec.rb +54 -0
- data/spec/record_evaluator_spec.rb +4 -0
- data/spec/record_finder_spec.rb +4 -0
- data/spec/record_spec.rb +100 -0
- data/spec/request_context_spec.rb +43 -0
- data/spec/request_count_limiter_spec.rb +28 -0
- data/spec/result_spec.rb +4 -0
- data/spec/rfc7208-tests.yml +2548 -0
- data/spec/sender_identity_spec.rb +69 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/term_spec.rb +38 -0
- data/spec/utils/domain_utils_spec.rb +60 -0
- data/spec/utils/host_utils_spec.rb +32 -0
- data/spec/utils/ip_in_domain_checker_spec.rb +4 -0
- data/spec/utils/validated_domain_finder_spec.rb +4 -0
- metadata +306 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require 'coppertone/mechanism/domain_spec_mechanism'
|
|
2
|
+
|
|
3
|
+
module Coppertone
|
|
4
|
+
class Mechanism # rubocop:disable Style/Documentation
|
|
5
|
+
class DomainSpecOptional < DomainSpecMechanism
|
|
6
|
+
def self.create(attributes)
|
|
7
|
+
new(attributes)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(attributes)
|
|
11
|
+
return if attributes.blank?
|
|
12
|
+
raw_domain_spec = trim_domain_spec(attributes)
|
|
13
|
+
@domain_spec = Coppertone::DomainSpec.new(raw_domain_spec)
|
|
14
|
+
rescue Coppertone::MacroStringParsingError
|
|
15
|
+
raise Coppertone::InvalidMechanismError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def match?(macro_context, request_context)
|
|
19
|
+
request_context.register_dns_lookup_term
|
|
20
|
+
target_name = generate_target_name(macro_context, request_context)
|
|
21
|
+
if target_name
|
|
22
|
+
match_target_name(macro_context, request_context, target_name)
|
|
23
|
+
else
|
|
24
|
+
handle_invalid_domain(macro_context, request_context)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def generate_target_name(macro_context, request_context)
|
|
29
|
+
if domain_spec
|
|
30
|
+
target_name_from_domain_spec(macro_context, request_context)
|
|
31
|
+
else
|
|
32
|
+
macro_context.domain
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def handle_invalid_domain(_macro_context, _options)
|
|
37
|
+
fail RecordParsingError
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ==(other)
|
|
41
|
+
return false unless other.instance_of? self.class
|
|
42
|
+
domain_spec == other.domain_spec
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Coppertone
|
|
2
|
+
class Mechanism # rubocop:disable Style/Documentation
|
|
3
|
+
class DomainSpecRequired < DomainSpecMechanism
|
|
4
|
+
def self.create(attributes)
|
|
5
|
+
new(attributes)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def initialize(attributes)
|
|
9
|
+
raw_domain_spec = trim_domain_spec(attributes)
|
|
10
|
+
fail InvalidMechanismError if raw_domain_spec.blank?
|
|
11
|
+
@domain_spec = Coppertone::DomainSpec.new(raw_domain_spec)
|
|
12
|
+
rescue Coppertone::MacroStringParsingError
|
|
13
|
+
raise Coppertone::InvalidMechanismError
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def match?(macro_context, request_context)
|
|
17
|
+
request_context.register_dns_lookup_term
|
|
18
|
+
target_name =
|
|
19
|
+
target_name_from_domain_spec(macro_context, request_context)
|
|
20
|
+
if target_name
|
|
21
|
+
match_target_name(macro_context, request_context, target_name)
|
|
22
|
+
else
|
|
23
|
+
handle_invalid_domain(macro_context, request_context)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def handle_invalid_domain(_macro_context, _options)
|
|
28
|
+
fail RecordParsingError
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def ==(other)
|
|
32
|
+
return false unless other.instance_of? self.class
|
|
33
|
+
domain_spec == other.domain_spec
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
require 'coppertone/mechanism/domain_spec_mechanism'
|
|
2
|
+
require 'ipaddr'
|
|
3
|
+
require 'coppertone/mechanism/cidr_parser'
|
|
4
|
+
|
|
5
|
+
module Coppertone
|
|
6
|
+
class Mechanism # rubocop:disable Style/Documentation
|
|
7
|
+
class DomainSpecWithDualCidr < DomainSpecMechanism
|
|
8
|
+
def self.create(attributes)
|
|
9
|
+
new(attributes)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(attributes)
|
|
13
|
+
return if attributes.blank?
|
|
14
|
+
parse_argument(attributes)
|
|
15
|
+
rescue Coppertone::MacroStringParsingError
|
|
16
|
+
raise Coppertone::InvalidMechanismError
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ip_v4_cidr_length
|
|
20
|
+
@ip_v4_cidr_length ||= 32
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ip_v6_cidr_length
|
|
24
|
+
@ip_v6_cidr_length ||= 128
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def cidr_length(macro_context)
|
|
28
|
+
if macro_context.original_ip_v6?
|
|
29
|
+
ip_v6_cidr_length
|
|
30
|
+
else
|
|
31
|
+
ip_v4_cidr_length
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def match?(macro_context, request_context)
|
|
36
|
+
request_context.register_dns_lookup_term
|
|
37
|
+
target_name = generate_target_name(macro_context, request_context)
|
|
38
|
+
if target_name
|
|
39
|
+
match_target_name(macro_context, request_context, target_name)
|
|
40
|
+
else
|
|
41
|
+
handle_invalid_domain(macro_context, request_context)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
CIDR_REGEXP = %r{(/(\d*))?(//(\d*))?\z}
|
|
46
|
+
def parse_argument(attributes)
|
|
47
|
+
fail InvalidMechanismError if attributes.blank?
|
|
48
|
+
cidr_matches = CIDR_REGEXP.match(attributes)
|
|
49
|
+
macro_string, raw_ip_v4_cidr_length, raw_ip_v6_cidr_length =
|
|
50
|
+
clean_matches(attributes, cidr_matches)
|
|
51
|
+
process_matches(macro_string, raw_ip_v4_cidr_length,
|
|
52
|
+
raw_ip_v6_cidr_length)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def parse_domain_spec(attributes, domain_spec_end)
|
|
56
|
+
return nil if attributes.blank?
|
|
57
|
+
cand = attributes[0..domain_spec_end]
|
|
58
|
+
return nil if cand.blank?
|
|
59
|
+
cand = trim_domain_spec(cand)
|
|
60
|
+
# At this point we've ascertained that there is
|
|
61
|
+
# a body to the domain spec
|
|
62
|
+
fail InvalidMechanismError if cand.blank?
|
|
63
|
+
cand
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def clean_matches(attributes, cidr_matches)
|
|
67
|
+
if cidr_matches
|
|
68
|
+
raw_ip_v4_cidr_length = cidr_matches[2] unless cidr_matches[2].blank?
|
|
69
|
+
raw_ip_v6_cidr_length = cidr_matches[4] unless cidr_matches[4].blank?
|
|
70
|
+
term = cidr_matches[0]
|
|
71
|
+
domain_spec_end = term.blank? ? -1 : (-1 - term.length)
|
|
72
|
+
else
|
|
73
|
+
domain_spec_end = -1
|
|
74
|
+
end
|
|
75
|
+
macro_string = parse_domain_spec(attributes, domain_spec_end)
|
|
76
|
+
[macro_string, raw_ip_v4_cidr_length, raw_ip_v6_cidr_length]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def process_matches(macro_string, raw_ip_v4_cidr_length,
|
|
80
|
+
raw_ip_v6_cidr_length)
|
|
81
|
+
@domain_spec = Coppertone::DomainSpec.new(macro_string) if macro_string
|
|
82
|
+
parse_v4_cidr_length(raw_ip_v4_cidr_length)
|
|
83
|
+
parse_v6_cidr_length(raw_ip_v6_cidr_length)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def parse_v4_cidr_length(raw_length)
|
|
87
|
+
@ip_v4_cidr_length = CidrParser.parse(raw_length, 32)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def parse_v6_cidr_length(raw_length)
|
|
91
|
+
@ip_v6_cidr_length = CidrParser.parse(raw_length, 128)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def generate_target_name(macro_context, request_context)
|
|
95
|
+
if domain_spec
|
|
96
|
+
target_name_from_domain_spec(macro_context, request_context)
|
|
97
|
+
else
|
|
98
|
+
macro_context.domain
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def handle_invalid_domain(_macro_context, _options)
|
|
103
|
+
fail RecordParsingError
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def ==(other)
|
|
107
|
+
return false unless other.instance_of? self.class
|
|
108
|
+
domain_spec == other.domain_spec &&
|
|
109
|
+
ip_v4_cidr_length == other.ip_v4_cidr_length &&
|
|
110
|
+
ip_v6_cidr_length == other.ip_v6_cidr_length
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'coppertone/mechanism/domain_spec_required'
|
|
2
|
+
|
|
3
|
+
module Coppertone
|
|
4
|
+
class Mechanism # rubocop:disable Style/Documentation
|
|
5
|
+
# Implements the exists mechanism.
|
|
6
|
+
class Exists < DomainSpecRequired
|
|
7
|
+
def match_target_name(_macro_context, request_context, target_name)
|
|
8
|
+
records = request_context.dns_client.fetch_a_records(target_name)
|
|
9
|
+
records.any?
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
register('exists', Coppertone::Mechanism::Exists)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'coppertone/mechanism/domain_spec_required'
|
|
2
|
+
require 'coppertone/mechanism/include_matcher'
|
|
3
|
+
require 'coppertone/record_finder'
|
|
4
|
+
|
|
5
|
+
module Coppertone
|
|
6
|
+
class Mechanism # rubocop:disable Style/Documentation
|
|
7
|
+
# Implements the include mechanism.
|
|
8
|
+
class Include < DomainSpecRequired
|
|
9
|
+
def match_target_name(macro_context, request_context, target_name)
|
|
10
|
+
context_for_include = macro_context.with_domain(target_name)
|
|
11
|
+
record =
|
|
12
|
+
RecordFinder.new(request_context.dns_client, target_name).record
|
|
13
|
+
IncludeMatcher.new(record).match?(context_for_include, request_context)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
register('include', Coppertone::Mechanism::Include)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'coppertone/record_evaluator'
|
|
2
|
+
|
|
3
|
+
module Coppertone
|
|
4
|
+
class Mechanism # rubocop:disable Style/Documentation
|
|
5
|
+
# Implements the include mechanism.
|
|
6
|
+
class IncludeMatcher
|
|
7
|
+
# Evaluates records that are referenced via an include
|
|
8
|
+
class IncludeRecordEvaluator < Coppertone::RecordEvaluator
|
|
9
|
+
def evaluate_fail_result(result, _m, _r)
|
|
10
|
+
result
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def evaluate_none_result(result, m, r)
|
|
14
|
+
new_result = super
|
|
15
|
+
return new_result unless new_result.none?
|
|
16
|
+
fail Coppertone::NoneIncludeResultError
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :record
|
|
21
|
+
def initialize(record)
|
|
22
|
+
@record = record
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def match?(macro_context, request_context)
|
|
26
|
+
fail Coppertone::NoneIncludeResultError if record.nil?
|
|
27
|
+
record_result =
|
|
28
|
+
IncludeRecordEvaluator.new(record)
|
|
29
|
+
.evaluate(macro_context, request_context)
|
|
30
|
+
record_result.pass?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require 'coppertone/mechanism/ip_mechanism'
|
|
2
|
+
|
|
3
|
+
module Coppertone
|
|
4
|
+
class Mechanism # rubocop:disable Style/Documentation
|
|
5
|
+
# Implements the ip4 mechanism.
|
|
6
|
+
class IP4 < IPMechanism
|
|
7
|
+
def ip_for_match(macro_context)
|
|
8
|
+
macro_context.ip_v4
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
register('ip4', Coppertone::Mechanism::IP4)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require 'coppertone/mechanism/ip_mechanism'
|
|
2
|
+
|
|
3
|
+
module Coppertone
|
|
4
|
+
class Mechanism # rubocop:disable Style/Documentation
|
|
5
|
+
# Implements the ip6 mechanism.
|
|
6
|
+
class IP6 < IPMechanism
|
|
7
|
+
def ip_for_match(macro_context)
|
|
8
|
+
macro_context.ip_v6
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
register('ip6', Coppertone::Mechanism::IP6)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Coppertone
|
|
2
|
+
class Mechanism # rubocop:disable Style/Documentation
|
|
3
|
+
# Implements the ip4 mechanism.
|
|
4
|
+
class IPMechanism < Mechanism
|
|
5
|
+
attr_reader :ip_network
|
|
6
|
+
def self.create(attributes)
|
|
7
|
+
new(attributes)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(attributes)
|
|
11
|
+
unless attributes.blank?
|
|
12
|
+
attributes = attributes[1..-1] if attributes[0] == ':'
|
|
13
|
+
@ip_network = parse_ip_network(attributes)
|
|
14
|
+
end
|
|
15
|
+
fail Coppertone::InvalidMechanismError if @ip_network.nil?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
LEADING_ZEROES_IN_CIDR_REGEXP = /\/0\d/
|
|
19
|
+
def validate_no_leading_zeroes_in_cidr(ip_as_s)
|
|
20
|
+
return unless LEADING_ZEROES_IN_CIDR_REGEXP.match(ip_as_s)
|
|
21
|
+
fail Coppertone::InvalidMechanismError
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parse_ip_network(ip_as_s)
|
|
25
|
+
validate_no_leading_zeroes_in_cidr(ip_as_s)
|
|
26
|
+
addr, cidr_length, dual = ip_as_s.split('/')
|
|
27
|
+
return nil if dual
|
|
28
|
+
network = IPAddr.new(addr)
|
|
29
|
+
network = network.mask(cidr_length.to_i) unless cidr_length.blank?
|
|
30
|
+
network
|
|
31
|
+
rescue IPAddr::Error
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def match?(macro_context, _request_context)
|
|
36
|
+
ip = ip_for_match(macro_context)
|
|
37
|
+
return false unless ip
|
|
38
|
+
return false unless ip.ipv4? == @ip_network.ipv4?
|
|
39
|
+
@ip_network.include?(ip)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def ==(other)
|
|
43
|
+
return false unless other.instance_of? self.class
|
|
44
|
+
ip_network == other.ip_network
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require 'coppertone/mechanism/domain_spec_with_dual_cidr'
|
|
2
|
+
|
|
3
|
+
module Coppertone
|
|
4
|
+
class Mechanism # rubocop:disable Style/Documentation
|
|
5
|
+
# Implements the MX mechanism.
|
|
6
|
+
class MX < DomainSpecWithDualCidr
|
|
7
|
+
def match_target_name(macro_context, request_context, target_name)
|
|
8
|
+
mx_exchange_names = look_up_mx_exchanges(request_context, target_name)
|
|
9
|
+
|
|
10
|
+
count = 0
|
|
11
|
+
checker = ip_checker(macro_context, request_context)
|
|
12
|
+
matched_record = mx_exchange_names.find do |mx|
|
|
13
|
+
count += 1
|
|
14
|
+
check_a_record_limit(request_context, count)
|
|
15
|
+
checker.check(mx, ip_v4_cidr_length, ip_v6_cidr_length)
|
|
16
|
+
end
|
|
17
|
+
!matched_record.nil?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ip_checker(macro_context, request_context)
|
|
21
|
+
Coppertone::Utils::IPInDomainChecker.new(macro_context,
|
|
22
|
+
request_context)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def look_up_mx_exchanges(request_context, target_name)
|
|
26
|
+
dns_client = request_context.dns_client
|
|
27
|
+
dns_client.fetch_mx_records(target_name).map do |mx|
|
|
28
|
+
mx[:exchange]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def check_a_record_limit(request_context, count)
|
|
33
|
+
limit = request_context.dns_lookups_per_mx_mechanism_limit
|
|
34
|
+
return unless limit && count > limit
|
|
35
|
+
fail Coppertone::MXLimitExceededError
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
register('mx', Coppertone::Mechanism::MX)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'coppertone/mechanism/domain_spec_optional'
|
|
2
|
+
|
|
3
|
+
module Coppertone
|
|
4
|
+
class Mechanism # rubocop:disable Style/Documentation
|
|
5
|
+
# Implements the ptr mechanism.
|
|
6
|
+
class Ptr < DomainSpecOptional
|
|
7
|
+
def match_target_name(macro_context, request_context, target_name)
|
|
8
|
+
matching_name =
|
|
9
|
+
Coppertone::Utils::ValidatedDomainFinder
|
|
10
|
+
.new(macro_context, request_context)
|
|
11
|
+
.find(target_name)
|
|
12
|
+
!matching_name.nil?
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
register('ptr', Coppertone::Mechanism::Ptr)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'coppertone/class_builder'
|
|
2
|
+
|
|
3
|
+
module Coppertone
|
|
4
|
+
# The class itself is primarily used as a factory for creating
|
|
5
|
+
# concrete mechanism instances based on a mechanism type
|
|
6
|
+
# string and corresponding attribute string. Validation of
|
|
7
|
+
# the type is done here, while validation of the attributes
|
|
8
|
+
# is delegated to the class corresponding to the
|
|
9
|
+
# mechanism type
|
|
10
|
+
class Mechanism
|
|
11
|
+
def self.class_builder
|
|
12
|
+
@class_builder ||= ClassBuilder.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.build(type, attributes)
|
|
16
|
+
class_builder.build(type, attributes)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.register(type, klass)
|
|
20
|
+
class_builder.register(type, klass)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
require 'coppertone/mechanism/a'
|
|
26
|
+
require 'coppertone/mechanism/all'
|
|
27
|
+
require 'coppertone/mechanism/exists'
|
|
28
|
+
require 'coppertone/mechanism/include'
|
|
29
|
+
require 'coppertone/mechanism/ip4'
|
|
30
|
+
require 'coppertone/mechanism/ip6'
|
|
31
|
+
require 'coppertone/mechanism/mx'
|
|
32
|
+
require 'coppertone/mechanism/ptr'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Coppertone
|
|
2
|
+
class Modifier # rubocop:disable Style/Documentation
|
|
3
|
+
class Base < Modifier
|
|
4
|
+
attr_reader :domain_spec
|
|
5
|
+
def initialize(attributes)
|
|
6
|
+
fail InvalidModifierError if attributes.blank?
|
|
7
|
+
@domain_spec = Coppertone::DomainSpec.new(attributes)
|
|
8
|
+
rescue Coppertone::MacroStringParsingError
|
|
9
|
+
raise Coppertone::InvalidModifierError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def target_name_from_domain_spec(macro_context, request_context)
|
|
13
|
+
domain =
|
|
14
|
+
domain_spec.expand(macro_context, request_context) if domain_spec
|
|
15
|
+
Coppertone::Utils::DomainUtils.macro_expanded_domain(domain)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def ==(other)
|
|
19
|
+
return false unless other.instance_of? self.class
|
|
20
|
+
domain_spec == other.domain_spec
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'coppertone/modifier/base'
|
|
2
|
+
|
|
3
|
+
module Coppertone
|
|
4
|
+
class Modifier # rubocop:disable Style/Documentation
|
|
5
|
+
class Exp < Coppertone::Modifier::Base
|
|
6
|
+
def self.create(attributes)
|
|
7
|
+
new(attributes)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
ASCII_REGEXP = /\A[[:ascii:]]*\z/
|
|
11
|
+
def evaluate(macro_context, request_context)
|
|
12
|
+
target_name =
|
|
13
|
+
target_name_from_domain_spec(macro_context, request_context)
|
|
14
|
+
return nil unless target_name
|
|
15
|
+
macro_string = lookup_macro_string(target_name, request_context)
|
|
16
|
+
return nil unless macro_string
|
|
17
|
+
expanded = macro_string.expand(macro_context, request_context)
|
|
18
|
+
return nil unless ASCII_REGEXP.match(expanded)
|
|
19
|
+
expanded
|
|
20
|
+
rescue Coppertone::Error
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def lookup_macro_string(target_name, request_context)
|
|
25
|
+
records =
|
|
26
|
+
request_context.dns_client.fetch_txt_records(target_name)
|
|
27
|
+
return nil if records.empty?
|
|
28
|
+
return nil if records.size > 1
|
|
29
|
+
MacroString.new(records.first[:text])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
register('exp', Coppertone::Modifier::Exp)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'coppertone/modifier/base'
|
|
2
|
+
|
|
3
|
+
module Coppertone
|
|
4
|
+
class Modifier # rubocop:disable Style/Documentation
|
|
5
|
+
class Redirect < Coppertone::Modifier::Base
|
|
6
|
+
def self.create(attributes)
|
|
7
|
+
new(attributes)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def evaluate(macro_context, request_context)
|
|
11
|
+
request_context.register_dns_lookup_term
|
|
12
|
+
target_name_from_domain_spec(macro_context, request_context)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
register('redirect', Coppertone::Modifier::Redirect)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Coppertone
|
|
2
|
+
class Modifier # rubocop:disable Style/Documentation
|
|
3
|
+
class Unknown < Modifier
|
|
4
|
+
def self.create(attributes)
|
|
5
|
+
new(attributes)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def initialize(attributes)
|
|
9
|
+
@macro_string = Coppertone::MacroString.new(attributes)
|
|
10
|
+
rescue Coppertone::MacroStringParsingError
|
|
11
|
+
raise Coppertone::InvalidModifierError
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
register('unknown', Coppertone::Modifier::Unknown)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Coppertone
|
|
2
|
+
# Instances of this class represent modifier terms, as defined by the
|
|
3
|
+
# SPF specification (see section 4.6.1).
|
|
4
|
+
class Modifier
|
|
5
|
+
def self.class_builder
|
|
6
|
+
@class_builder ||= ClassBuilder.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.build(type, attributes)
|
|
10
|
+
class_builder.build(type, attributes)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.register(type, klass)
|
|
14
|
+
class_builder.register(type, klass)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
MODIFIER_REGEXP = /\A([a-zA-Z]+[a-zA-Z0-9\-\_\.]*)=(\S*)\z/
|
|
18
|
+
def self.matching_term(text)
|
|
19
|
+
matches = MODIFIER_REGEXP.match(text)
|
|
20
|
+
return nil unless matches
|
|
21
|
+
type = matches[1]
|
|
22
|
+
attributes = matches[2]
|
|
23
|
+
build(type, attributes) || build('unknown', attributes)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
require 'coppertone/modifier/exp'
|
|
29
|
+
require 'coppertone/modifier/redirect'
|
|
30
|
+
require 'coppertone/modifier/unknown'
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require 'coppertone/result'
|
|
2
|
+
|
|
3
|
+
module Coppertone
|
|
4
|
+
# Instances of this class represent qualifiers, as defined by the
|
|
5
|
+
# SPF specification (see section 4.6.1).
|
|
6
|
+
#
|
|
7
|
+
# There are only 4 qualifiers permitted by the specification, so
|
|
8
|
+
# this class does not allow the creation of new instances. These
|
|
9
|
+
# fixed instances should be accessed through either the class level
|
|
10
|
+
# constants or the qualifiers class method.
|
|
11
|
+
class Qualifier
|
|
12
|
+
@qualifier_hash = {}
|
|
13
|
+
|
|
14
|
+
DEFAULT_QUALIFIER_TEXT = '+'.freeze
|
|
15
|
+
def self.find_by_text(text)
|
|
16
|
+
text = DEFAULT_QUALIFIER_TEXT if text.blank?
|
|
17
|
+
@qualifier_hash[text]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.default_qualifier
|
|
21
|
+
find_by_text(nil)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attr_reader :text, :result_code
|
|
25
|
+
def initialize(text, result_code)
|
|
26
|
+
@text = text
|
|
27
|
+
@result_code = result_code
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
PASS = new(DEFAULT_QUALIFIER_TEXT, Result::PASS)
|
|
31
|
+
FAIL = new('-'.freeze, Result::FAIL)
|
|
32
|
+
SOFTFAIL = new('~'.freeze, Result::SOFTFAIL)
|
|
33
|
+
NEUTRAL = new('?'.freeze, Result::NEUTRAL)
|
|
34
|
+
|
|
35
|
+
private_class_method :new
|
|
36
|
+
|
|
37
|
+
def self.qualifiers
|
|
38
|
+
[PASS, FAIL, SOFTFAIL, NEUTRAL]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
qualifiers.each do |q|
|
|
42
|
+
@qualifier_hash[q.text] = q
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|