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