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,110 @@
1
+ require 'resolv'
2
+ require 'resolv/dns/resource/in/spf'
3
+
4
+ module Coppertone
5
+ module DNS
6
+ # An adapter client for the internal Resolv DNS client.
7
+ class ResolvClient
8
+ def fetch_a_records(domain)
9
+ fetch_a_type_records(domain, 'A')
10
+ end
11
+
12
+ def fetch_aaaa_records(domain)
13
+ fetch_a_type_records(domain, 'AAAA')
14
+ end
15
+
16
+ def fetch_mx_records(domain)
17
+ fetch_records(domain, 'MX') do |record|
18
+ {
19
+ type: 'MX',
20
+ exchange: record.exchange.to_s
21
+ }
22
+ end
23
+ end
24
+
25
+ def fetch_ptr_records(arpa_address)
26
+ fetch_records(arpa_address, 'PTR') do |record|
27
+ {
28
+ type: 'PTR',
29
+ name: record.name.to_s
30
+ }
31
+ end
32
+ end
33
+
34
+ def fetch_txt_records(domain)
35
+ fetch_txt_type_records(domain, 'TXT')
36
+ end
37
+
38
+ def fetch_spf_records(domain)
39
+ fetch_txt_type_records(domain, 'SPF')
40
+ end
41
+
42
+ private
43
+
44
+ def fetch_a_type_records(domain, type)
45
+ fetch_records(domain, type) do |record|
46
+ {
47
+ type: type,
48
+ address: record.address.to_s
49
+ }
50
+ end
51
+ end
52
+
53
+ def fetch_txt_type_records(domain, type)
54
+ fetch_records(domain, type) do |record|
55
+ {
56
+ type: type,
57
+ # Use strings.join('') to avoid JRuby issue where
58
+ # data only returns the first string
59
+ text: record.strings.join('')
60
+ }
61
+ end
62
+ end
63
+
64
+ def fetch_records(domain, type, &block)
65
+ records = dns_lookup(domain, type)
66
+ records.map(&block)
67
+ end
68
+
69
+ TRAILING_DOT_REGEXP = /\.\z/
70
+ def normalize_domain(domain)
71
+ (domain.sub(TRAILING_DOT_REGEXP, '') || domain).downcase
72
+ end
73
+
74
+ def dns_lookup(domain, rr_type)
75
+ domain = normalize_domain(domain)
76
+ resources = getresources(domain, rr_type)
77
+
78
+ unless resources
79
+ fail Coppertone::DNS::Error,
80
+ "Unknown error on DNS '#{rr_type}' lookup of '#{domain}'"
81
+ end
82
+
83
+ resources
84
+ end
85
+
86
+ def getresources(domain, rr_type)
87
+ rr_class = self.class.type_class(rr_type)
88
+ dns_resolver.getresources(domain, rr_class)
89
+ rescue Resolv::ResolvTimeout
90
+ raise Coppertone::DNS::TimeoutError,
91
+ "Time-out on DNS '#{rr_type}' lookup of '#{domain}'"
92
+ rescue Resolv::ResolvError
93
+ raise Coppertone::DNS::Error, "Error on DNS lookup of '#{domain}'"
94
+ end
95
+
96
+ SUPPORTED_RR_TYPES = %w(A AAAA MX PTR TXT SPF)
97
+ def self.type_class(rr_type)
98
+ if SUPPORTED_RR_TYPES.include?(rr_type)
99
+ Resolv::DNS::Resource::IN.const_get(rr_type)
100
+ else
101
+ fail ArgumentError, "Unknown RR type: #{rr_type}"
102
+ end
103
+ end
104
+
105
+ def dns_resolver
106
+ @dns_resolver ||= Resolv::DNS.new
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,3 @@
1
+ require 'coppertone/dns/error'
2
+ require 'coppertone/dns/resolv_client'
3
+ require 'coppertone/dns/mock_client'
@@ -0,0 +1,45 @@
1
+ require 'coppertone/macro_string'
2
+ require 'coppertone/utils'
3
+
4
+ module Coppertone
5
+ # A domain spec, as defined in the SPF specification.
6
+ class DomainSpec < MacroString
7
+ def initialize(s)
8
+ begin
9
+ super
10
+ rescue Coppertone::MacroStringParsingError
11
+ raise Coppertone::DomainSpecParsingError
12
+ end
13
+ validate_domain_spec_restrictions
14
+ end
15
+
16
+ def validate_domain_spec_restrictions
17
+ return if only_allowed_macros? && ends_in_allowed_term?
18
+ fail Coppertone::DomainSpecParsingError
19
+ end
20
+
21
+ EXP_ONLY_MACRO_LETTERS = %w(c r t)
22
+ def only_allowed_macros?
23
+ @macros.select { |m| m.is_a?(Coppertone::MacroString::MacroExpand) }
24
+ .none? { |m| EXP_ONLY_MACRO_LETTERS.include?(m.macro_letter) }
25
+ end
26
+
27
+ def ends_in_allowed_term?
28
+ lm = @macros.last
29
+ return true unless lm
30
+ return false if lm.is_a?(Coppertone::MacroString::MacroStaticExpand)
31
+ return true if lm.is_a?(Coppertone::MacroString::MacroExpand)
32
+ ends_with_top_label?
33
+ end
34
+
35
+ def ends_with_top_label?
36
+ ends_with = @macros.last.to_s
37
+ ends_with = ends_with[0..-2] if ends_with[-1] == '.'
38
+ _, match, tail = ends_with.rpartition('.')
39
+ return false if match.blank?
40
+ hostname = Coppertone::Utils::DomainUtils.valid_hostname_label?(tail)
41
+ return false unless hostname
42
+ true
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,29 @@
1
+ module Coppertone
2
+ class Error < ::StandardError
3
+ end
4
+
5
+ class TemperrorError < Coppertone::Error; end
6
+ class PermerrorError < Coppertone::Error; end
7
+
8
+ class InvalidSenderError < Coppertone::Error; end
9
+ class MissingNameError < Coppertone::PermerrorError; end
10
+ class MissingQualifierError < Coppertone::PermerrorError; end
11
+
12
+ class MacroStringParsingError < Coppertone::PermerrorError; end
13
+ class DomainSpecParsingError < MacroStringParsingError; end
14
+
15
+ class RecordParsingError < Coppertone::PermerrorError; end
16
+ class InvalidMechanismError < Coppertone::RecordParsingError; end
17
+ class InvalidModifierError < Coppertone::RecordParsingError; end
18
+
19
+ class MissingSpfRecordError < Coppertone::Error; end
20
+ class AmbiguousSpfRecordError < Coppertone::PermerrorError; end
21
+
22
+ class NoneIncludeResultError < Coppertone::PermerrorError; end
23
+ class InvalidRedirectError < Coppertone::PermerrorError; end
24
+
25
+ class LimitExceededError < Coppertone::PermerrorError; end
26
+ class TermLimitExceededError < PermerrorError; end
27
+ class VoidLimitExceededError < PermerrorError; end
28
+ class MXLimitExceededError < PermerrorError; end
29
+ end
@@ -0,0 +1,75 @@
1
+ require 'ipaddr'
2
+
3
+ module Coppertone
4
+ # A wrapper class for the IP address of the SMTP client that is emitting
5
+ # the email and is being validated by the SPF process. This class
6
+ # contains a number of helper methods designed to support the use
7
+ # of IPs in mechanism evaluation and macro string evaluation.
8
+ #
9
+ # Note: This class should only be used with a single IP address, and
10
+ # will fail if passed an address with a prefix
11
+ class IPAddressWrapper
12
+ attr_reader :string_representation, :ip
13
+ def initialize(s)
14
+ @ip = self.class.parse(s)
15
+ fail ArgumentError unless @ip
16
+ @string_representation = s
17
+ end
18
+
19
+ def self.parse(s)
20
+ return nil unless s
21
+ return nil if s.index('/')
22
+ ip_addr = IPAddr.new(s)
23
+ normalize_ip(ip_addr)
24
+ rescue IPAddr::InvalidAddressError
25
+ nil
26
+ end
27
+
28
+ def self.normalize_ip(parsed_ip)
29
+ return parsed_ip unless parsed_ip && parsed_ip.ipv6?
30
+ parsed_ip.ipv4_mapped? ? parsed_ip.native : parsed_ip
31
+ end
32
+
33
+ def to_dotted_notation
34
+ if original_ipv6?
35
+ format('%.32x', @ip.to_i).split(//).join('.').upcase
36
+ elsif original_ipv4?
37
+ @ip.to_s
38
+ end
39
+ end
40
+ alias_method :i, :to_dotted_notation
41
+
42
+ def to_human_readable
43
+ @ip.to_s
44
+ end
45
+ alias_method :c, :to_human_readable
46
+
47
+ def v
48
+ original_ipv4? ? 'in-addr' : 'ip6'
49
+ end
50
+
51
+ def p
52
+ fail NotImplementedError
53
+ end
54
+
55
+ def ip_v4
56
+ original_ipv4? ? @ip : nil
57
+ end
58
+
59
+ def ip_v6
60
+ original_ipv6? ? @ip : nil
61
+ end
62
+
63
+ def original_ipv4?
64
+ @ip.ipv4?
65
+ end
66
+
67
+ def original_ipv6?
68
+ @ip.ipv6?
69
+ end
70
+
71
+ def to_s
72
+ @string_representation
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,67 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+ require 'addressable/uri'
3
+ require 'ostruct'
4
+ require 'uri'
5
+
6
+ module Coppertone
7
+ # A context used to evaluate MacroStrings. Responds to all of the
8
+ # macro letter directives.
9
+ class MacroContext
10
+ attr_reader :domain, :ip_address_wrapper, :sender_identity, :helo_domain
11
+
12
+ delegate :s, :l, :o, to: :sender_identity
13
+ alias_method :d, :domain
14
+ delegate :i, :p, :v, :c, to: :ip_address_wrapper
15
+ delegate :ip_v4, :ip_v6, :original_ipv4?, :original_ipv6?,
16
+ to: :ip_address_wrapper
17
+ alias_method :h, :helo_domain
18
+
19
+ attr_reader :hostname
20
+ def initialize(domain, ip_as_s, sender, helo_domain = 'unknown',
21
+ options = {})
22
+ @ip_address_wrapper = IPAddressWrapper.new(ip_as_s)
23
+ @sender_identity = SenderIdentity.new(sender)
24
+ @domain = domain || @sender_identity.domain
25
+ @helo_domain = helo_domain
26
+ @hostname = options[:hostname]
27
+ end
28
+
29
+ UNKNOWN_HOSTNAME = 'unknown'
30
+ def r
31
+ if Coppertone::Utils::DomainUtils.valid?(raw_hostname)
32
+ raw_hostname
33
+ else
34
+ UNKNOWN_HOSTNAME
35
+ end
36
+ end
37
+
38
+ def t
39
+ Time.now.to_i
40
+ end
41
+
42
+ %w(s l o d i p v h c r t).each do |m|
43
+ define_method(m.upcase) do
44
+ unencoded = send(m)
45
+ if unencoded
46
+ ::URI.escape(unencoded, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
47
+ else
48
+ nil
49
+ end
50
+ end
51
+ end
52
+
53
+ def raw_hostname
54
+ @raw_hostname ||=
55
+ (hostname || Coppertone.config.hostname ||
56
+ Coppertone::Utils::HostUtils.hostname)
57
+ end
58
+
59
+ # Generates a new MacroContext with all the same info, but a new
60
+ # domain
61
+ def with_domain(new_domain)
62
+ options = {}
63
+ options[:hostname] = hostname if hostname
64
+ MacroContext.new(new_domain, c, s, h, options)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,84 @@
1
+ require 'coppertone/error'
2
+
3
+ module Coppertone
4
+ class MacroString
5
+ # A internal class that represents a term in the MacroString that
6
+ # may need to be expanded based on the SPF request context. This
7
+ # class validates against the Macro Definitions defined in section
8
+ # 7.2, as well as against the set of delimiters, transformers, and
9
+ # grammer defined in section 7.1.
10
+ class MacroExpand
11
+ MACRO_LETTER_CHAR_SET = '[slodiphcrtvSLODIPHCRTV]'
12
+ PTR_MACRO_CHAR_SET = %w(p P)
13
+ DELIMITER_CHAR_SET = '[\.\-\+\,\/\_\=]'
14
+ VALID_BODY_REGEXP =
15
+ /\A(#{MACRO_LETTER_CHAR_SET})(\d*)(r?)(#{DELIMITER_CHAR_SET}*)\z/
16
+
17
+ attr_reader :macro_letter, :digit_transformers, :reverse,
18
+ :delimiter_regexp
19
+ alias_method :reverse?, :reverse
20
+ def initialize(s)
21
+ matches = VALID_BODY_REGEXP.match(s)
22
+ fail Coppertone::MacroStringParsingError if matches.nil?
23
+ @macro_letter = matches[1]
24
+ initialize_digit_transformers(matches[2])
25
+ @reverse = (matches[3] == 'r')
26
+ initialize_delimiter(matches[4])
27
+ @body = s
28
+ end
29
+
30
+ def initialize_digit_transformers(raw_value)
31
+ return unless raw_value
32
+ @digit_transformers = raw_value.to_i if raw_value.length > 0
33
+ return unless @digit_transformers
34
+ fail Coppertone::MacroStringParsingError if @digit_transformers == 0
35
+ end
36
+
37
+ def ptr_macro?
38
+ PTR_MACRO_CHAR_SET.include?(@macro_letter)
39
+ end
40
+
41
+ def expand_ptr(context, request)
42
+ ptr =
43
+ Coppertone::Utils::ValidatedDomainFinder
44
+ .new(context, request).find(context.d)
45
+ return 'unknown' unless ptr
46
+ @macro_letter == 'P' ? ::Addressable::URI.encode_component(ptr) : ptr
47
+ end
48
+
49
+ def raw_value(context, request)
50
+ ptr_macro? ? expand_ptr(context, request) : context.send(@macro_letter)
51
+ end
52
+
53
+ def expand(context, request = nil)
54
+ labels = raw_value(context, request).split(@delimiter_regexp)
55
+ labels.reverse! if @reverse
56
+ labels = labels.last(@digit_transformers) if @digit_transformers
57
+ labels.join(DEFAULT_DELIMITER)
58
+ end
59
+
60
+ def to_s
61
+ @body
62
+ end
63
+
64
+ def ==(other)
65
+ return false unless other.instance_of? self.class
66
+ to_s == other.to_s
67
+ end
68
+
69
+ private
70
+
71
+ DEFAULT_DELIMITER = '.'
72
+ def initialize_delimiter(raw_delimiter)
73
+ delimiter_chars =
74
+ if raw_delimiter && raw_delimiter.length >= 1
75
+ raw_delimiter
76
+ else
77
+ DEFAULT_DELIMITER
78
+ end
79
+ @delimiter_regexp =
80
+ Regexp.new("[#{delimiter_chars}]")
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,24 @@
1
+ module Coppertone
2
+ class MacroString
3
+ # A internal class that represents a fixed string in the macro template,
4
+ # whose value will not depend on the SPF request context.
5
+ class MacroLiteral
6
+ def initialize(s)
7
+ @str = s
8
+ end
9
+
10
+ def expand(_context, _request = nil)
11
+ @str
12
+ end
13
+
14
+ def to_s
15
+ @str
16
+ end
17
+
18
+ def ==(other)
19
+ return false unless other.instance_of? self.class
20
+ to_s == other.to_s
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,62 @@
1
+ require 'coppertone/macro_string/macro_literal'
2
+ require 'coppertone/macro_string/macro_expand'
3
+ require 'coppertone/macro_string/macro_static_expand'
4
+
5
+ module Coppertone
6
+ class MacroString
7
+ # A internal class that parses the macro string template into
8
+ # an object that can later be evaluated (or 'expanded')
9
+ # in the context of a particular SPF check.
10
+ class MacroParser
11
+ attr_reader :macros
12
+ def initialize(s)
13
+ @s = s.dup
14
+ @macros = []
15
+ parse_macro_array
16
+ end
17
+
18
+ def parse_macro_array
19
+ while @s && @s.length > 0
20
+ if starting_macro?
21
+ parse_interpolated_macro
22
+ else
23
+ parse_macro_literal
24
+ end
25
+ end
26
+ end
27
+
28
+ def starting_macro?
29
+ @s && @s.length >= 1 && (@s[0] == '%')
30
+ end
31
+
32
+ def parse_contextual_interpolated_macro
33
+ fail MacroStringParsingError unless @s[1] == '{'
34
+ closing_index = @s.index('}')
35
+ fail MacroStringParsingError unless closing_index
36
+ interpolated_body = @s[2, closing_index - 2]
37
+ @macros << MacroExpand.new(interpolated_body)
38
+ @s = @s[(closing_index + 1)..-1]
39
+ end
40
+
41
+ SIMPLE_MACRO_LETTERS = %w(% _ -)
42
+ def parse_interpolated_macro
43
+ fail MacroStringParsingError if @s.length == 1
44
+ macro_code = @s[0, 2]
45
+ if MacroStaticExpand.exists_for?(macro_code)
46
+ @macros << MacroStaticExpand.macro_for(macro_code)
47
+ @s = @s[2..-1]
48
+ else
49
+ parse_contextual_interpolated_macro
50
+ end
51
+ end
52
+
53
+ def parse_macro_literal
54
+ new_idx = @s.index('%')
55
+ new_idx ||= @s.length
56
+ @macros << MacroLiteral.new(@s[0, new_idx])
57
+ @s = @s[new_idx..-1]
58
+ new_idx
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,52 @@
1
+ require 'coppertone/error'
2
+
3
+ module Coppertone
4
+ class MacroString
5
+ # A internal class that represents one of a few special terms in the macro
6
+ # string definition. These terms include a '%', but do not depend on the
7
+ # SPF request context.
8
+ class MacroStaticExpand
9
+ def initialize(macro_text, s)
10
+ @macro_text = macro_text
11
+ @str = s
12
+ end
13
+
14
+ def expand(_context, _request = nil)
15
+ @str
16
+ end
17
+
18
+ def to_s
19
+ @macro_text
20
+ end
21
+
22
+ # Replaces '%%' in a macro string
23
+ PERCENT_MACRO = new('%%'.freeze, '%'.freeze)
24
+
25
+ # Replaces '%_' in a macro string
26
+ SPACE_MACRO = new('%_'.freeze, ' '.freeze)
27
+
28
+ # Replaces '%-' in a macro string
29
+ URL_ENCODED_SPACE_MACRO = new('%-'.freeze, '%20'.freeze)
30
+
31
+ SIMPLE_INTERPOLATED_MACRO_LETTERS = %w(% _ -).freeze
32
+ def self.exists_for?(x)
33
+ return false unless x && (x.length == 2) && (x[0] == '%')
34
+ SIMPLE_INTERPOLATED_MACRO_LETTERS.include?(x[1])
35
+ end
36
+
37
+ def self.macro_for(x)
38
+ fail Coppertone::MacroStringParsingError unless exists_for?(x)
39
+ case x[1]
40
+ when '%'
41
+ PERCENT_MACRO
42
+ when '_'
43
+ SPACE_MACRO
44
+ when '-'
45
+ URL_ENCODED_SPACE_MACRO
46
+ end
47
+ end
48
+
49
+ private_class_method :new
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,31 @@
1
+ require 'coppertone/macro_string/macro_parser'
2
+
3
+ module Coppertone
4
+ # Instances of this class represent macro-strings, as defined by the
5
+ # SPF specification (see section 7.1).
6
+ #
7
+ # MacroStrings should be evaluated ('expanded') in a particular context,
8
+ # as the MacroString may use of a number of values available from the
9
+ # context for interpolation.
10
+ class MacroString
11
+ attr_reader :macro_text
12
+ def initialize(macro_text)
13
+ @macro_text = macro_text
14
+ parse_macros
15
+ end
16
+
17
+ def parse_macros
18
+ # Build an array of expandable macros
19
+ @macros = MacroParser.new(macro_text).macros
20
+ end
21
+
22
+ def expand(context, request = nil)
23
+ @macros.map { |m| m.expand(context, request) }.join('')
24
+ end
25
+
26
+ def ==(other)
27
+ return false unless other.instance_of? self.class
28
+ macro_text == other.macro_text
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ require 'coppertone/mechanism/domain_spec_with_dual_cidr'
2
+ require 'coppertone/utils/ip_in_domain_checker'
3
+
4
+ module Coppertone
5
+ class Mechanism # rubocop:disable Style/Documentation
6
+ # Implements the A mechanism.
7
+ class A < DomainSpecWithDualCidr
8
+ def match_target_name(macro_context, request_context, target_name)
9
+ Coppertone::Utils::IPInDomainChecker
10
+ .new(macro_context, request_context)
11
+ .check(target_name, ip_v4_cidr_length, ip_v6_cidr_length)
12
+ end
13
+ end
14
+ register('a', Coppertone::Mechanism::A)
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ module Coppertone
2
+ class Mechanism # rubocop:disable Style/Documentation
3
+ # Implements the All mechanism. To reduce unnecessary object creation, this
4
+ # class is a singleton since all All mechanisms behave identically.
5
+ class All < Mechanism
6
+ SINGLETON = new
7
+ private_class_method :new
8
+
9
+ def self.create(attributes)
10
+ fail InvalidMechanismError unless attributes.blank?
11
+ SINGLETON
12
+ end
13
+
14
+ def self.instance
15
+ SINGLETON
16
+ end
17
+
18
+ def match?(_macro_context, _request_context)
19
+ true
20
+ end
21
+ end
22
+ register('all', Coppertone::Mechanism::All)
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ module Coppertone
2
+ class Mechanism # rubocop:disable Style/Documentation
3
+ class CidrParser
4
+ def self.parse(raw_length, max_val)
5
+ return if raw_length.blank?
6
+ length_as_i = raw_length.to_i
7
+ if length_as_i < 0 || length_as_i > max_val
8
+ fail Coppertone::InvalidMechanismError
9
+ end
10
+ length_as_i.to_s
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ module Coppertone
2
+ class Mechanism # rubocop:disable Style/Documentation
3
+ class DomainSpecMechanism < Mechanism
4
+ attr_reader :domain_spec
5
+
6
+ def target_name_from_domain_spec(macro_context, request_context)
7
+ domain =
8
+ domain_spec.expand(macro_context, request_context) if domain_spec
9
+ Coppertone::Utils::DomainUtils.macro_expanded_domain(domain)
10
+ end
11
+
12
+ def trim_domain_spec(raw_domain_spec)
13
+ return nil if raw_domain_spec.blank?
14
+ raw_domain_spec[1..-1]
15
+ end
16
+ end
17
+ end
18
+ end