coppertone 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +34 -0
- data/.travis.yml +7 -0
- data/Gemfile +7 -0
- data/LICENSE +201 -0
- data/README.md +58 -0
- data/Rakefile +140 -0
- data/coppertone.gemspec +27 -0
- data/lib/coppertone/class_builder.rb +20 -0
- data/lib/coppertone/directive.rb +38 -0
- data/lib/coppertone/dns/error.rb +9 -0
- data/lib/coppertone/dns/mock_client.rb +106 -0
- data/lib/coppertone/dns/resolv_client.rb +110 -0
- data/lib/coppertone/dns.rb +3 -0
- data/lib/coppertone/domain_spec.rb +45 -0
- data/lib/coppertone/error.rb +29 -0
- data/lib/coppertone/ip_address_wrapper.rb +75 -0
- data/lib/coppertone/macro_context.rb +67 -0
- data/lib/coppertone/macro_string/macro_expand.rb +84 -0
- data/lib/coppertone/macro_string/macro_literal.rb +24 -0
- data/lib/coppertone/macro_string/macro_parser.rb +62 -0
- data/lib/coppertone/macro_string/macro_static_expand.rb +52 -0
- data/lib/coppertone/macro_string.rb +31 -0
- data/lib/coppertone/mechanism/a.rb +16 -0
- data/lib/coppertone/mechanism/all.rb +24 -0
- data/lib/coppertone/mechanism/cidr_parser.rb +14 -0
- data/lib/coppertone/mechanism/domain_spec_mechanism.rb +18 -0
- data/lib/coppertone/mechanism/domain_spec_optional.rb +46 -0
- data/lib/coppertone/mechanism/domain_spec_required.rb +37 -0
- data/lib/coppertone/mechanism/domain_spec_with_dual_cidr.rb +114 -0
- data/lib/coppertone/mechanism/exists.rb +14 -0
- data/lib/coppertone/mechanism/include.rb +18 -0
- data/lib/coppertone/mechanism/include_matcher.rb +34 -0
- data/lib/coppertone/mechanism/ip4.rb +13 -0
- data/lib/coppertone/mechanism/ip6.rb +13 -0
- data/lib/coppertone/mechanism/ip_mechanism.rb +48 -0
- data/lib/coppertone/mechanism/mx.rb +40 -0
- data/lib/coppertone/mechanism/ptr.rb +17 -0
- data/lib/coppertone/mechanism.rb +32 -0
- data/lib/coppertone/modifier/base.rb +24 -0
- data/lib/coppertone/modifier/exp.rb +34 -0
- data/lib/coppertone/modifier/redirect.rb +17 -0
- data/lib/coppertone/modifier/unknown.rb +16 -0
- data/lib/coppertone/modifier.rb +30 -0
- data/lib/coppertone/qualifier.rb +45 -0
- data/lib/coppertone/record.rb +86 -0
- data/lib/coppertone/record_evaluator.rb +63 -0
- data/lib/coppertone/record_finder.rb +34 -0
- data/lib/coppertone/request.rb +68 -0
- data/lib/coppertone/request_context.rb +67 -0
- data/lib/coppertone/request_count_limiter.rb +36 -0
- data/lib/coppertone/result.rb +50 -0
- data/lib/coppertone/sender_identity.rb +39 -0
- data/lib/coppertone/spf_service.rb +9 -0
- data/lib/coppertone/term.rb +13 -0
- data/lib/coppertone/utils/domain_utils.rb +59 -0
- data/lib/coppertone/utils/host_utils.rb +22 -0
- data/lib/coppertone/utils/ip_in_domain_checker.rb +53 -0
- data/lib/coppertone/utils/validated_domain_finder.rb +40 -0
- data/lib/coppertone/utils.rb +4 -0
- data/lib/coppertone/version.rb +3 -0
- data/lib/coppertone.rb +48 -0
- data/lib/resolv/dns/resource/in/spf.rb +15 -0
- data/spec/directive_spec.rb +41 -0
- data/spec/dns/resolv_client_spec.rb +307 -0
- data/spec/domain_spec_spec.rb +35 -0
- data/spec/ip_address_wrapper_spec.rb +67 -0
- data/spec/macro_context_spec.rb +69 -0
- data/spec/macro_string/macro_expand_spec.rb +79 -0
- data/spec/macro_string/macro_literal_spec.rb +27 -0
- data/spec/macro_string/macro_static_expand_spec.rb +67 -0
- data/spec/macro_string_spec.rb +20 -0
- data/spec/mechanism/a_spec.rb +198 -0
- data/spec/mechanism/all_spec.rb +22 -0
- data/spec/mechanism/exists_spec.rb +91 -0
- data/spec/mechanism/include_spec.rb +43 -0
- data/spec/mechanism/ip4_spec.rb +110 -0
- data/spec/mechanism/ip6_spec.rb +104 -0
- data/spec/mechanism/mx_spec.rb +51 -0
- data/spec/mechanism/ptr_spec.rb +43 -0
- data/spec/mechanism_spec.rb +4 -0
- data/spec/modifier_spec.rb +4 -0
- data/spec/open_spf/ALL_mechanism_syntax_spec.rb +38 -0
- data/spec/open_spf/A_mechanism_syntax_spec.rb +159 -0
- data/spec/open_spf/EXISTS_mechanism_syntax_spec.rb +46 -0
- data/spec/open_spf/IP4_mechanism_syntax_spec.rb +59 -0
- data/spec/open_spf/IP6_mechanism_syntax_spec.rb +60 -0
- data/spec/open_spf/Include_mechanism_semantics_and_syntax_spec.rb +56 -0
- data/spec/open_spf/Initial_processing_spec.rb +77 -0
- data/spec/open_spf/MX_mechanism_syntax_spec.rb +119 -0
- data/spec/open_spf/Macro_expansion_rules_spec.rb +154 -0
- data/spec/open_spf/PTR_mechanism_syntax_spec.rb +42 -0
- data/spec/open_spf/Processing_limits_spec.rb +72 -0
- data/spec/open_spf/Record_evaluation_spec.rb +75 -0
- data/spec/open_spf/Record_lookup_spec.rb +48 -0
- data/spec/open_spf/Selecting_records_spec.rb +61 -0
- data/spec/open_spf/Semantics_of_exp_and_other_modifiers_spec.rb +167 -0
- data/spec/open_spf/Test_cases_from_implementation_bugs_spec.rb +17 -0
- data/spec/qualifier_spec.rb +54 -0
- data/spec/record_evaluator_spec.rb +4 -0
- data/spec/record_finder_spec.rb +4 -0
- data/spec/record_spec.rb +100 -0
- data/spec/request_context_spec.rb +43 -0
- data/spec/request_count_limiter_spec.rb +28 -0
- data/spec/result_spec.rb +4 -0
- data/spec/rfc7208-tests.yml +2548 -0
- data/spec/sender_identity_spec.rb +69 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/term_spec.rb +38 -0
- data/spec/utils/domain_utils_spec.rb +60 -0
- data/spec/utils/host_utils_spec.rb +32 -0
- data/spec/utils/ip_in_domain_checker_spec.rb +4 -0
- data/spec/utils/validated_domain_finder_spec.rb +4 -0
- metadata +306 -0
|
@@ -0,0 +1,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,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
|