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