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
@@ -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