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