coppertone 0.0.1

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