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,86 @@
1
+ module Coppertone
2
+ # Represents an SPF record. Includes class level methods for parsing
3
+ # record from a text string.
4
+ class Record
5
+ VERSION_STR = 'v=spf1'
6
+ RECORD_REGEXP = /\A#{VERSION_STR}(\s|\z)/i
7
+ ALLOWED_CHARACTERS = /\A([\x21-\x7e ]+)\z/
8
+
9
+ attr_reader :text
10
+ def initialize(raw_text)
11
+ fail RecordParsingError if raw_text.blank?
12
+ fail RecordParsingError unless self.class.record?(raw_text)
13
+ fail RecordParsingError unless ALLOWED_CHARACTERS.match(raw_text)
14
+ @text = raw_text.dup
15
+ validate_and_parse
16
+ end
17
+
18
+ def self.record?(record_text)
19
+ return false if record_text.blank?
20
+ RECORD_REGEXP.match(record_text.strip) ? true : false
21
+ end
22
+
23
+ def self.parse(text)
24
+ return nil unless record?(text)
25
+ new(text)
26
+ end
27
+
28
+ def validate_and_parse
29
+ text_without_prefix = text[VERSION_STR.length..-1]
30
+ @term_tokens = text_without_prefix.strip.split(/ /)
31
+ parse_terms
32
+ end
33
+
34
+ def parse_terms
35
+ @terms = []
36
+ @term_tokens.each do |token|
37
+ term = Term.build_from_token(token)
38
+ fail RecordParsingError,
39
+ "Could not parse record with #{text}" unless term
40
+ @terms << term
41
+ end
42
+ normalize_terms
43
+ end
44
+
45
+ def normalize_terms
46
+ # Discard any redirects if there is a directive with an
47
+ # all mechanism present
48
+ # Section 6.1
49
+ # TODO: PMG
50
+ find_redirect # Checks for duplicate redirect modifiers
51
+ exp # Checks for duplicate exp modifiers
52
+ end
53
+
54
+ def directives
55
+ @directives ||= @terms.select { |t| t.is_a?(Coppertone::Directive) }
56
+ end
57
+
58
+ def include_all?
59
+ directives.any? { |d| d.mechanism.is_a?(Coppertone::Mechanism::All) }
60
+ end
61
+
62
+ def modifiers
63
+ @modifiers ||= @terms.select { |t| t.is_a?(Coppertone::Modifier) }
64
+ end
65
+
66
+ def find_redirect
67
+ find_modifier(Coppertone::Modifier::Redirect)
68
+ end
69
+
70
+ def redirect
71
+ # Ignore if an 'all' modifier is present
72
+ return nil if include_all?
73
+ @redirect ||= find_redirect
74
+ end
75
+
76
+ def exp
77
+ @exp ||= find_modifier(Coppertone::Modifier::Exp)
78
+ end
79
+
80
+ def find_modifier(klass)
81
+ arr = modifiers.select { |m| m.is_a?(klass) }
82
+ fail RecordParsingError if arr.size > 1
83
+ arr.first
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,63 @@
1
+ module Coppertone
2
+ # A helper class for finding SPF records for a domain.
3
+ class RecordEvaluator
4
+ attr_reader :record
5
+ def initialize(record)
6
+ @record = record
7
+ end
8
+
9
+ def evaluate(macro_context, request_context)
10
+ result = directive_result(macro_context, request_context)
11
+ return result unless result.none? || result.fail?
12
+ if result.fail?
13
+ evaluate_fail_result(result, macro_context, request_context)
14
+ else
15
+ # Evaluate redirect
16
+ evaluate_none_result(result, macro_context, request_context)
17
+ end
18
+ end
19
+
20
+ def directive_result(macro_context, request_context)
21
+ record.directives.reduce(Result.none) do |memo, d|
22
+ memo.none? ? d.evaluate(macro_context, request_context) : memo
23
+ end
24
+ end
25
+
26
+ def evaluate_fail_result(result, macro_context, request_context)
27
+ add_exp_to_result(result, macro_context, request_context)
28
+ end
29
+
30
+ def add_exp_to_result(result, macro_context, request_context)
31
+ result = add_default_exp(result)
32
+ return result unless record.exp
33
+ computed_exp = record.exp.evaluate(macro_context, request_context)
34
+ result.explanation = computed_exp if computed_exp
35
+ result
36
+ rescue Coppertone::Error
37
+ result
38
+ end
39
+
40
+ def add_default_exp(result)
41
+ result.explanation = Coppertone.config.default_explanation
42
+ result
43
+ end
44
+
45
+ def follow_redirect?
46
+ # Ignore the redirect if there's an all
47
+ # mechanism in the record
48
+ record.redirect && !record.include_all?
49
+ end
50
+
51
+ def evaluate_none_result(result, macro_context, request_context)
52
+ return result unless follow_redirect?
53
+ redirect_target = record.redirect.evaluate(macro_context, request_context)
54
+ if redirect_target
55
+ redirect_record =
56
+ RecordFinder.new(request_context.dns_client, redirect_target).record
57
+ end
58
+ fail InvalidRedirectError unless redirect_record
59
+ rc = macro_context.with_domain(redirect_target)
60
+ RecordEvaluator.new(redirect_record).evaluate(rc, request_context)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,34 @@
1
+ module Coppertone
2
+ # A helper class for finding SPF records for a domain.
3
+ class RecordFinder
4
+ attr_reader :dns_client, :domain
5
+ def initialize(dns_client, domain)
6
+ @dns_client = dns_client
7
+ @domain = domain
8
+ end
9
+
10
+ def record
11
+ @record ||=
12
+ begin
13
+ validate_txt_records
14
+ Record.parse(txt_records.first)
15
+ end
16
+ end
17
+
18
+ def txt_records
19
+ @txt_records ||=
20
+ begin
21
+ if Coppertone::Utils::DomainUtils.valid?(domain)
22
+ dns_client.fetch_txt_records(domain).map { |r| r[:text] }
23
+ .select { |r| Record.record?(r) }
24
+ else
25
+ []
26
+ end
27
+ end
28
+ end
29
+
30
+ def validate_txt_records
31
+ fail AmbiguousSpfRecordError if txt_records.size > 1
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,68 @@
1
+ module Coppertone
2
+ # Represents an SPF request.
3
+ class Request
4
+ attr_reader :ip_as_s, :sender, :helo_domain, :options, :result
5
+ attr_reader :helo_result, :mailfrom_result
6
+ def initialize(ip_as_s, sender, helo_domain, options = {})
7
+ @ip_as_s = ip_as_s
8
+ @sender = sender
9
+ @helo_domain = helo_domain
10
+ @options = options
11
+ end
12
+
13
+ def authenticate
14
+ check_spf_for_helo
15
+ return helo_result if helo_result && !helo_result.none?
16
+
17
+ check_spf_for_mailfrom
18
+ return mailfrom_result if mailfrom_result && !mailfrom_result.none?
19
+
20
+ no_matching_record? ? Result.none : Result.new(:neutral)
21
+ end
22
+
23
+ def no_matching_record?
24
+ helo_result.nil? && mailfrom_result.nil?
25
+ end
26
+
27
+ def check_spf_for_helo
28
+ @helo_result ||= check_spf_for_context(helo_context, 'helo')
29
+ end
30
+
31
+ def check_spf_for_mailfrom
32
+ @mailfrom_result ||= check_spf_for_context(mailfrom_context, 'mailfrom')
33
+ end
34
+
35
+ def check_spf_for_context(macro_context, identity)
36
+ record = spf_record(macro_context)
37
+ @result = spf_request(macro_context, record, identity) if record
38
+ rescue Coppertone::TemperrorError => e
39
+ Result.temperror(e.message)
40
+ rescue Coppertone::PermerrorError => e
41
+ Result.permerror(e.message)
42
+ end
43
+
44
+ def request_context
45
+ @request_context ||= RequestContext.new(options)
46
+ end
47
+
48
+ def helo_context
49
+ MacroContext.new(helo_domain, ip_as_s, sender, helo_domain, options)
50
+ end
51
+
52
+ def mailfrom_context
53
+ MacroContext.new(nil, ip_as_s, sender, helo_domain, options)
54
+ end
55
+
56
+ def spf_record(macro_context)
57
+ RecordFinder.new(request_context.dns_client,
58
+ macro_context.domain).record
59
+ end
60
+
61
+ def spf_request(macro_context, record, identity)
62
+ return Result.new(:none) if record.nil?
63
+ r = RecordEvaluator.new(record).evaluate(macro_context, request_context)
64
+ r.identity = identity
65
+ r
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,67 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+ require 'coppertone/dns/resolv_client'
3
+
4
+ module Coppertone
5
+ # A container for information that should span the lifetime of
6
+ # an SPF check. This include the DNS client, the locale used
7
+ # for error messages, limits for DNS requests of different
8
+ # types, and limiters that ensure those limits are not exceeded
9
+ # across the lifetime of the request.
10
+ class RequestContext
11
+ def initialize(options = {})
12
+ @options = (options || {}).dup
13
+ end
14
+
15
+ def register_dns_lookup_term
16
+ dns_lookup_term_count_limiter.increment!
17
+ end
18
+
19
+ def register_void_dns_result
20
+ void_dns_result_count_limiter.increment!
21
+ end
22
+
23
+ def dns_lookups_per_mx_mechanism_limit
24
+ config_value(:dns_lookups_per_mx_mechanism_limit)
25
+ end
26
+
27
+ def dns_lookups_per_ptr_mechanism_limit
28
+ config_value(:dns_lookups_per_ptr_mechanism_limit)
29
+ end
30
+
31
+ def message_locale
32
+ config_value(:message_locale)
33
+ end
34
+
35
+ def dns_client
36
+ @dns_client ||=
37
+ if @options[:dns_client]
38
+ @options[:dns_client]
39
+ elsif @options[:dns_client_class]
40
+ @options[:dns_client_class].new
41
+ elsif Coppertone.config.dns_client_class
42
+ Coppertone.config.dns_client_class.new
43
+ else
44
+ Coppertone::DNS::ResolvClient.new
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def dns_lookup_term_count_limiter
51
+ limit = config_value(:terms_requiring_dns_lookup_limit)
52
+ @dns_lookup_term_count_limiter ||=
53
+ Coppertone::RequestCountLimiter.new(limit, 'DNS lookup term')
54
+ end
55
+
56
+ def void_dns_result_count_limiter
57
+ limit = config_value(:void_dns_result_limit)
58
+ @void_dns_result_count_limiter ||=
59
+ Coppertone::RequestCountLimiter.new(limit, 'DNS lookup term')
60
+ end
61
+
62
+ def config_value(k)
63
+ return @options[k] if @options.key?(k)
64
+ Coppertone.config.send(k)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,36 @@
1
+ module Coppertone
2
+ # A utility class that encapsulates counter and limit behavior. Primarily
3
+ # used to track and limit the number of DNS queries of various types.
4
+ class RequestCountLimiter
5
+ attr_accessor :count, :limit, :counter_description
6
+ def initialize(limit = nil, counter_description = nil)
7
+ self.limit = limit
8
+ self.counter_description = counter_description
9
+ self.count = 0
10
+ end
11
+
12
+ def increment!(num = 1)
13
+ self.count += num
14
+ check_if_limit_exceeded
15
+ count
16
+ end
17
+
18
+ def check_if_limit_exceeded
19
+ return if limit.nil?
20
+ fail Coppertone::LimitExceededError, exception_message if exceeded?
21
+ end
22
+
23
+ def exception_message
24
+ "Maximum #{counter_description} limit of #{limit} exceeded."
25
+ end
26
+
27
+ def exceeded?
28
+ return false unless limited?
29
+ count > limit
30
+ end
31
+
32
+ def limited?
33
+ !limit.nil?
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,50 @@
1
+ module Coppertone
2
+ # The result of an SPF query. Includes a code, which indicates the
3
+ # overall result (pass, fail, softfail, etc.). For different
4
+ # results it may include the mechanism which led to the result,
5
+ # an error message, and/or an explanation string.
6
+ class Result
7
+ NONE = :none
8
+
9
+ PASS = :pass
10
+ FAIL = :fail
11
+ SOFTFAIL = :softfail
12
+ NEUTRAL = :neutral
13
+
14
+ TEMPERROR = :temperror
15
+ PERMERROR = :permerror
16
+
17
+ attr_reader :code, :mechanism
18
+ attr_accessor :explanation, :problem, :identity
19
+ def initialize(code, mechanism = nil)
20
+ @code = code
21
+ @mechanism = mechanism
22
+ end
23
+
24
+ def self.from_directive(directive, code)
25
+ new(code, directive.mechanism)
26
+ end
27
+
28
+ def self.permerror(message)
29
+ r = Result.new(:permerror)
30
+ r.problem = message
31
+ r
32
+ end
33
+
34
+ def self.temperror(message)
35
+ r = Result.new(:temperror)
36
+ r.problem = message
37
+ r
38
+ end
39
+
40
+ def self.none
41
+ Result.new(:none)
42
+ end
43
+
44
+ %w(none pass fail softfail neutral temperror permerror).each do |t|
45
+ define_method("#{t}?") do
46
+ self.class.const_get(t.upcase) == send(:code)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,39 @@
1
+ module Coppertone
2
+ # A consolidated sender identity, suitable for use with an SPF request.
3
+ # Parses the identity and ensures validity. Also has accessor methods
4
+ # for the macro letters.
5
+ class SenderIdentity
6
+ DEFAULT_LOCALPART = 'postmaster'
7
+ EMAIL_ADDRESS_SPLIT_REGEXP = /^(.*)@(.*?)$/
8
+
9
+ attr_reader :sender, :localpart, :domain
10
+ def initialize(sender)
11
+ @sender = sender
12
+ initialize_localpart_and_domain
13
+ end
14
+
15
+ alias_method :s, :sender
16
+ alias_method :l, :localpart
17
+ alias_method :o, :domain
18
+
19
+ private
20
+
21
+ def initialize_localpart(matches)
22
+ localpart_candidate = matches[1] if matches
23
+ @localpart =
24
+ localpart_candidate.blank? ? DEFAULT_LOCALPART : localpart_candidate
25
+ end
26
+
27
+ def initialize_domain(matches)
28
+ domain_candidate = matches[2] if matches
29
+ @domain =
30
+ domain_candidate.blank? ? sender : domain_candidate
31
+ end
32
+
33
+ def initialize_localpart_and_domain
34
+ matches = EMAIL_ADDRESS_SPLIT_REGEXP.match(sender)
35
+ initialize_localpart(matches)
36
+ initialize_domain(matches)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,9 @@
1
+ module Coppertone
2
+ # Service interface for SPF authentication
3
+ class SpfService
4
+ def self.authenticate_email(ip_as_s, sender, helo_domain, options = {})
5
+ req = Coppertone::Request.new(ip_as_s, sender, helo_domain, options)
6
+ req.authenticate
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module Coppertone
2
+ # Instances of this class represent terms as defined in section 4.6.1 of
3
+ # the specification. The Term class should be considered abstract,
4
+ # and should only be instantiated as its concrete subclasses. Terms
5
+ # are generally parsed from text tokens in an SPF TXT record using the
6
+ # factory method in this class.
7
+ class Term
8
+ def self.build_from_token(token)
9
+ return nil unless token
10
+ Directive.matching_term(token) || Modifier.matching_term(token)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,59 @@
1
+ require 'addressable/idna'
2
+
3
+ module Coppertone
4
+ module Utils
5
+ # A utility class that includes methods for working with
6
+ # domain names.
7
+ class DomainUtils
8
+ def self.valid?(domain)
9
+ return false if domain.blank?
10
+ labels = to_labels(domain)
11
+ return false if labels.length <= 1
12
+ return false if domain.length > 253
13
+ return false if labels.any? { |l| !valid_label?(l) }
14
+ true
15
+ end
16
+
17
+ def self.to_labels(domain)
18
+ Addressable::IDNA.to_ascii(domain).split('.')
19
+ end
20
+
21
+ NO_DASH_REGEXP = /\A[a-zA-Z0-9]*[a-zA-Z]+[a-zA-Z0-9]*\z/
22
+ DASH_REGEXP = /\A[a-zA-Z0-9]+\-[a-zA-Z0-9\-]*[a-zA-Z0-9]+\z/
23
+
24
+ def self.valid_hostname_label?(l)
25
+ return false unless valid_label?(l)
26
+ NO_DASH_REGEXP.match(l) || DASH_REGEXP.match(l)
27
+ end
28
+
29
+ def self.valid_label?(l)
30
+ (l.length >= 0) && (l.length <= 63)
31
+ end
32
+
33
+ def self.macro_expanded_domain(domain)
34
+ return nil if domain.blank?
35
+ labels = to_labels(domain)
36
+ domain = labels.join('.')
37
+ while domain.length > 253
38
+ labels = labels.drop(1)
39
+ domain = labels.join('.')
40
+ end
41
+ domain
42
+ end
43
+
44
+ def self.subdomain_of?(subdomain_candidate, domain)
45
+ subdomain_labels = to_labels(subdomain_candidate)
46
+ domain_labels = to_labels(domain)
47
+ num_labels_in_domain = domain_labels.length
48
+ return false if subdomain_labels.length <= domain_labels.length
49
+ subdomain_labels.last(num_labels_in_domain) == domain_labels
50
+ end
51
+
52
+ def self.subdomain_or_same?(candidate, domain)
53
+ return false unless valid?(domain) && valid?(candidate)
54
+ return true if domain == candidate
55
+ subdomain_of?(candidate, domain)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,22 @@
1
+ require 'addressable/idna'
2
+
3
+ module Coppertone
4
+ module Utils
5
+ # A utility class that includes methods for working with
6
+ # data about the host.
7
+ class HostUtils
8
+ def self.hostname
9
+ @hostname ||=
10
+ begin
11
+ Socket.gethostbyname(Socket.gethostname).first
12
+ rescue SocketError
13
+ Socket.gethostname
14
+ end
15
+ end
16
+
17
+ def self.clear_hostname
18
+ @hostname = nil
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,53 @@
1
+ module Coppertone
2
+ module Utils # rubocop:disable Style/Documentation
3
+ class IPInDomainChecker
4
+ def initialize(macro_context, request_context)
5
+ @macro_context = macro_context
6
+ @request_context = request_context
7
+ end
8
+
9
+ def check(domain_name,
10
+ ip_v4_cidr_length = 32,
11
+ ip_v6_cidr_length = 128)
12
+ cidr_length = ip_v6? ? ip_v6_cidr_length : ip_v4_cidr_length
13
+ networks = ip_networks(domain_name, cidr_length)
14
+ @request_context.register_void_dns_result if networks.empty?
15
+
16
+ matching_network =
17
+ networks.find do |network|
18
+ network.include?(ip)
19
+ end
20
+ !matching_network.nil?
21
+ end
22
+
23
+ def ip_v6?
24
+ @macro_context.original_ipv6?
25
+ end
26
+
27
+ def ip
28
+ ip_v6? ? @macro_context.ip_v6 : @macro_context.ip_v4
29
+ end
30
+
31
+ def ip_networks(domain_name, cidr_length)
32
+ ip_records =
33
+ if ip_v6?
34
+ dns_client.fetch_aaaa_records(domain_name)
35
+ else
36
+ dns_client.fetch_a_records(domain_name)
37
+ end
38
+ filtered_records(ip_records, cidr_length)
39
+ end
40
+
41
+ def dns_client
42
+ @request_context.dns_client
43
+ end
44
+
45
+ def filtered_records(recs, cidr_length)
46
+ ips = recs.map do |r|
47
+ IPAddr.new(r[:address]).mask(cidr_length.to_i)
48
+ end
49
+ ips.select { |i| !i.nil? }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,40 @@
1
+ module Coppertone
2
+ module Utils # rubocop:disable Style/Documentation
3
+ # A class used to find validated domains as defined in
4
+ # section 5.5 of the RFC.
5
+ class ValidatedDomainFinder
6
+ def initialize(macro_context, request_context)
7
+ @mc = macro_context
8
+ @request_context = request_context
9
+ end
10
+
11
+ def find(target_name)
12
+ ip = @mc.original_ipv6? ? @mc.ip_v6 : @mc.ip_v4
13
+ ptr_names = fetch_ptr_names(ip)
14
+ ip_checker = IPInDomainChecker.new(@mc, @request_context)
15
+ ptr_names.find { |n| ptr_record_matches?(ip_checker, target_name, n) }
16
+ end
17
+
18
+ def fetch_ptr_names(ip)
19
+ dns_client = @request_context.dns_client
20
+ names = dns_client.fetch_ptr_records(ip.reverse).map do |ptr|
21
+ ptr[:name]
22
+ end
23
+ record_limit =
24
+ @request_context.dns_lookups_per_ptr_mechanism_limit
25
+ record_limit ? names.slice(0, record_limit) : names
26
+ end
27
+
28
+ def ptr_record_matches?(ip_checker,
29
+ target_name, ptr_name)
30
+ is_candidate =
31
+ DomainUtils.subdomain_or_same?(ptr_name, target_name)
32
+ is_candidate && ip_checker.check(ptr_name)
33
+ rescue Coppertone::DNS::Error
34
+ # If a DNS error occurs when looking up a domain, treat it
35
+ # as a non match
36
+ false
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,4 @@
1
+ require 'coppertone/utils/domain_utils'
2
+ require 'coppertone/utils/host_utils'
3
+ require 'coppertone/utils/ip_in_domain_checker'
4
+ require 'coppertone/utils/validated_domain_finder'
@@ -0,0 +1,3 @@
1
+ module Coppertone # rubocop:disable Style/Documentation
2
+ VERSION = '0.0.1'.freeze
3
+ end