coppertone 0.0.7 → 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +79 -0
  3. data/.codeclimate.yml +28 -0
  4. data/.github/dependabot.yml +7 -0
  5. data/.rubocop.yml +47 -6
  6. data/.travis.yml +2 -6
  7. data/Gemfile +12 -2
  8. data/LICENSE +21 -201
  9. data/Rakefile +12 -6
  10. data/coppertone.gemspec +9 -9
  11. data/lib/coppertone/class_builder.rb +2 -0
  12. data/lib/coppertone/directive.rb +6 -1
  13. data/lib/coppertone/domain_spec.rb +5 -1
  14. data/lib/coppertone/error.rb +5 -0
  15. data/lib/coppertone/ip_address_wrapper.rb +5 -1
  16. data/lib/coppertone/macro_context.rb +11 -4
  17. data/lib/coppertone/macro_string.rb +4 -2
  18. data/lib/coppertone/macro_string/macro_expand.rb +6 -2
  19. data/lib/coppertone/macro_string/macro_literal.rb +1 -0
  20. data/lib/coppertone/macro_string/macro_parser.rb +8 -4
  21. data/lib/coppertone/macro_string/macro_static_expand.rb +3 -1
  22. data/lib/coppertone/mechanism.rb +2 -0
  23. data/lib/coppertone/mechanism/all.rb +1 -0
  24. data/lib/coppertone/mechanism/cidr_parser.rb +3 -3
  25. data/lib/coppertone/mechanism/domain_spec_mechanism.rb +10 -3
  26. data/lib/coppertone/mechanism/domain_spec_optional.rb +1 -0
  27. data/lib/coppertone/mechanism/domain_spec_required.rb +1 -0
  28. data/lib/coppertone/mechanism/domain_spec_with_dual_cidr.rb +8 -1
  29. data/lib/coppertone/mechanism/include_matcher.rb +3 -0
  30. data/lib/coppertone/mechanism/ip_mechanism.rb +20 -10
  31. data/lib/coppertone/mechanism/mx.rb +1 -0
  32. data/lib/coppertone/modifier.rb +3 -1
  33. data/lib/coppertone/modifier/base.rb +7 -2
  34. data/lib/coppertone/modifier/exp.rb +6 -2
  35. data/lib/coppertone/modifier/redirect.rb +1 -0
  36. data/lib/coppertone/modifier/unknown.rb +1 -0
  37. data/lib/coppertone/null_macro_context.rb +1 -1
  38. data/lib/coppertone/qualifier.rb +1 -0
  39. data/lib/coppertone/record.rb +6 -2
  40. data/lib/coppertone/record_evaluator.rb +5 -0
  41. data/lib/coppertone/record_finder.rb +2 -2
  42. data/lib/coppertone/record_term_parser.rb +9 -5
  43. data/lib/coppertone/redirect_record_finder.rb +3 -1
  44. data/lib/coppertone/request.rb +26 -23
  45. data/lib/coppertone/request_context.rb +1 -0
  46. data/lib/coppertone/request_count_limiter.rb +2 -0
  47. data/lib/coppertone/result.rb +2 -1
  48. data/lib/coppertone/sender_identity.rb +2 -1
  49. data/lib/coppertone/term.rb +1 -0
  50. data/lib/coppertone/terms_parser.rb +3 -1
  51. data/lib/coppertone/utils/domain_utils.rb +21 -8
  52. data/lib/coppertone/utils/ip_in_domain_checker.rb +1 -1
  53. data/lib/coppertone/utils/validated_domain_finder.rb +2 -1
  54. data/lib/coppertone/version.rb +1 -1
  55. data/spec/domain_spec_spec.rb +1 -1
  56. data/spec/macro_string/macro_static_expand_spec.rb +2 -2
  57. data/spec/mechanism/ip4_spec.rb +3 -0
  58. data/spec/mechanism/ip6_spec.rb +3 -0
  59. data/spec/null_macro_context_spec.rb +1 -1
  60. data/spec/open_spf/Initial_processing_spec.rb +0 -2
  61. data/spec/open_spf/Semantics_of_exp_and_other_modifiers_spec.rb +0 -2
  62. data/spec/record_spec.rb +1 -1
  63. data/spec/spec_helper.rb +14 -3
  64. data/spec/term_spec.rb +1 -1
  65. data/spec/utils/domain_utils_spec.rb +10 -2
  66. metadata +34 -32
@@ -2,6 +2,7 @@ module Coppertone
2
2
  # A helper class for finding SPF records for a redirect modifier.
3
3
  class RedirectRecordFinder
4
4
  attr_reader :redirect, :macro_context, :request_context
5
+
5
6
  def initialize(redirect, macro_context, request_context)
6
7
  @redirect = redirect
7
8
  @macro_context = macro_context
@@ -9,11 +10,12 @@ module Coppertone
9
10
  end
10
11
 
11
12
  def target
12
- @redirect_target ||= redirect.evaluate(macro_context, request_context)
13
+ @target ||= redirect.evaluate(macro_context, request_context)
13
14
  end
14
15
 
15
16
  def record
16
17
  return unless target
18
+
17
19
  @record ||= RecordFinder.new(request_context.dns_client, target).record
18
20
  end
19
21
  end
@@ -1,8 +1,8 @@
1
1
  module Coppertone
2
2
  # Represents an SPF request.
3
3
  class Request
4
- attr_reader :ip_as_s, :sender, :helo_domain, :options, :result
5
- attr_reader :helo_result, :mailfrom_result
4
+ attr_reader :ip_as_s, :sender, :helo_domain, :options, :result, :helo_result, :mailfrom_result
5
+
6
6
  def initialize(ip_as_s, sender, helo_domain, options = {})
7
7
  @ip_as_s = ip_as_s
8
8
  @sender = sender
@@ -16,13 +16,15 @@ module Coppertone
16
16
 
17
17
  def process_helo
18
18
  check_spf_for_helo
19
- return nil if helo_result && helo_result.none?
19
+ return nil if helo_result&.none?
20
+
20
21
  helo_result
21
22
  end
22
23
 
23
24
  def process_mailfrom
24
25
  check_spf_for_mailfrom
25
- return nil if mailfrom_result && mailfrom_result.none?
26
+ return nil if mailfrom_result&.none?
27
+
26
28
  mailfrom_result
27
29
  end
28
30
 
@@ -34,25 +36,6 @@ module Coppertone
34
36
  helo_result.nil? && mailfrom_result.nil?
35
37
  end
36
38
 
37
- def check_spf_for_helo
38
- @helo_result ||= check_spf_for_context(helo_context, 'helo')
39
- end
40
-
41
- def check_spf_for_mailfrom
42
- @mailfrom_result ||= check_spf_for_context(mailfrom_context, 'mailfrom')
43
- end
44
-
45
- def check_spf_for_context(macro_context, identity)
46
- record = spf_record(macro_context)
47
- @result = spf_request(macro_context, record, identity) if record
48
- rescue DNSAdapter::Error => e
49
- Result.temperror(e.message)
50
- rescue Coppertone::TemperrorError => e
51
- Result.temperror(e.message)
52
- rescue Coppertone::PermerrorError => e
53
- Result.permerror(e.message)
54
- end
55
-
56
39
  def request_context
57
40
  @request_context ||= RequestContext.new(options)
58
41
  end
@@ -72,9 +55,29 @@ module Coppertone
72
55
 
73
56
  def spf_request(macro_context, record, identity)
74
57
  return Result.new(:none) if record.nil?
58
+
75
59
  r = RecordEvaluator.new(record).evaluate(macro_context, request_context)
76
60
  r.identity = identity
77
61
  r
78
62
  end
63
+
64
+ private
65
+
66
+ def check_spf_for_helo
67
+ @helo_result = check_spf_for_context(helo_context, 'helo')
68
+ end
69
+
70
+ def check_spf_for_mailfrom
71
+ @mailfrom_result = check_spf_for_context(mailfrom_context, 'mailfrom')
72
+ end
73
+
74
+ def check_spf_for_context(macro_context, identity)
75
+ record = spf_record(macro_context)
76
+ @result = spf_request(macro_context, record, identity) if record
77
+ rescue DNSAdapter::Error, Coppertone::TemperrorError => e
78
+ Result.temperror(e.message)
79
+ rescue Coppertone::PermerrorError => e
80
+ Result.permerror(e.message)
81
+ end
79
82
  end
80
83
  end
@@ -60,6 +60,7 @@ module Coppertone
60
60
 
61
61
  def config_value(k)
62
62
  return @options[k] if @options.key?(k)
63
+
63
64
  Coppertone.config.send(k)
64
65
  end
65
66
  end
@@ -3,6 +3,7 @@ module Coppertone
3
3
  # used to track and limit the number of DNS queries of various types.
4
4
  class RequestCountLimiter
5
5
  attr_accessor :count, :limit, :counter_description
6
+
6
7
  def initialize(limit = nil, counter_description = nil)
7
8
  self.limit = limit
8
9
  self.counter_description = counter_description
@@ -26,6 +27,7 @@ module Coppertone
26
27
 
27
28
  def exceeded?
28
29
  return false unless limited?
30
+
29
31
  count > limit
30
32
  end
31
33
 
@@ -16,6 +16,7 @@ module Coppertone
16
16
 
17
17
  attr_reader :code, :mechanism
18
18
  attr_accessor :explanation, :problem, :identity
19
+
19
20
  def initialize(code, mechanism = nil)
20
21
  @code = code
21
22
  @mechanism = mechanism
@@ -45,7 +46,7 @@ module Coppertone
45
46
  Result.new(:neutral)
46
47
  end
47
48
 
48
- %w(none pass fail softfail neutral temperror permerror).each do |t|
49
+ %w[none pass fail softfail neutral temperror permerror].each do |t|
49
50
  define_method("#{t}?") do
50
51
  self.class.const_get(t.upcase) == send(:code)
51
52
  end
@@ -4,9 +4,10 @@ module Coppertone
4
4
  # for the macro letters.
5
5
  class SenderIdentity
6
6
  DEFAULT_LOCALPART = 'postmaster'.freeze
7
- EMAIL_ADDRESS_SPLIT_REGEXP = /^(.*)@(.*?)$/
7
+ EMAIL_ADDRESS_SPLIT_REGEXP = /^(.*)@(.*?)$/.freeze
8
8
 
9
9
  attr_reader :sender, :localpart, :domain
10
+
10
11
  def initialize(sender)
11
12
  @sender = sender
12
13
  initialize_localpart_and_domain
@@ -7,6 +7,7 @@ module Coppertone
7
7
  class Term
8
8
  def self.build_from_token(token)
9
9
  return nil unless token
10
+
10
11
  Directive.matching_term(token) || Modifier.matching_term(token)
11
12
  end
12
13
  end
@@ -2,6 +2,7 @@ module Coppertone
2
2
  # Parses a un-prefixed string into terms
3
3
  class TermsParser
4
4
  attr_reader :text
5
+
5
6
  def initialize(text)
6
7
  @text = text
7
8
  end
@@ -11,12 +12,13 @@ module Coppertone
11
12
  end
12
13
 
13
14
  def tokens
14
- text.split(/ /).select { |s| !s.blank? }
15
+ text.split(/ /).reject(&:blank?)
15
16
  end
16
17
 
17
18
  def parse_token(token)
18
19
  term = Term.build_from_token(token)
19
20
  raise RecordParsingError unless term
21
+
20
22
  term
21
23
  end
22
24
  end
@@ -7,12 +7,17 @@ module Coppertone
7
7
  class DomainUtils
8
8
  def self.valid?(domain)
9
9
  return false if domain.blank?
10
- labels = to_ascii_labels(domain)
11
- return false if labels.length <= 1
12
10
  return false if domain.length > 253
13
- return false if labels.any? { |l| !valid_label?(l) }
11
+
12
+ labels_valid?(to_ascii_labels(domain))
13
+ end
14
+
15
+ def self.labels_valid?(labels)
16
+ return false if labels.length <= 1
17
+ return false if labels.first == '*'
14
18
  return false unless valid_tld?(labels.last)
15
- true
19
+
20
+ labels.all? { |l| valid_label?(l) }
16
21
  end
17
22
 
18
23
  def self.to_labels(domain)
@@ -22,6 +27,7 @@ module Coppertone
22
27
  def self.parent_domain(domain)
23
28
  labels = to_labels(domain)
24
29
  return '.' if labels.size == 1
30
+
25
31
  labels.shift
26
32
  labels.join('.')
27
33
  end
@@ -34,33 +40,38 @@ module Coppertone
34
40
  to_ascii_labels(domain).join('.')
35
41
  end
36
42
 
37
- NO_DASH_NONNUMERIC_REGEXP = /\A[a-zA-Z0-9]*[a-zA-Z]+[a-zA-Z0-9]*\z/
38
- NO_DASH_REGEXP = /\A[a-zA-Z0-9]+\z/
39
- DASH_REGEXP = /\A[a-zA-Z0-9]+\-[a-zA-Z0-9\-]*[a-zA-Z0-9]+\z/
43
+ NO_DASH_NONNUMERIC_REGEXP = /\A[a-zA-Z0-9]*[a-zA-Z]+[a-zA-Z0-9]*\z/.freeze
44
+ NO_DASH_REGEXP = /\A[a-zA-Z0-9]+\z/.freeze
45
+ DASH_REGEXP = /\A[a-zA-Z0-9]+-[a-zA-Z0-9\-]*[a-zA-Z0-9]+\z/.freeze
40
46
 
41
47
  def self.valid_hostname_label?(l)
42
48
  return false unless valid_label?(l)
49
+
43
50
  NO_DASH_REGEXP.match(l) || DASH_REGEXP.match(l)
44
51
  end
45
52
 
46
53
  def self.valid_tld?(l)
47
54
  return false unless valid_label?(l)
55
+
48
56
  NO_DASH_NONNUMERIC_REGEXP.match(l) || DASH_REGEXP.match(l)
49
57
  end
50
58
 
51
59
  def self.valid_ldh_domain?(domain)
52
60
  return false unless valid?(domain)
61
+
53
62
  labels = to_ascii_labels(domain)
54
63
  return false unless valid_tld?(labels.last)
64
+
55
65
  labels.all? { |l| valid_hostname_label?(l) }
56
66
  end
57
67
 
58
68
  def self.valid_label?(l)
59
- (l.length >= 0) && (l.length <= 63) && !l.match(/\s/)
69
+ !l.empty? && (l.length <= 63) && !l.match(/\s/)
60
70
  end
61
71
 
62
72
  def self.macro_expanded_domain(domain)
63
73
  return nil if domain.blank?
74
+
64
75
  labels = to_ascii_labels(domain)
65
76
  domain = labels.join('.')
66
77
  while domain.length > 253
@@ -75,12 +86,14 @@ module Coppertone
75
86
  domain_labels = to_ascii_labels(domain)
76
87
  num_labels_in_domain = domain_labels.length
77
88
  return false if subdomain_labels.length <= domain_labels.length
89
+
78
90
  subdomain_labels.last(num_labels_in_domain) == domain_labels
79
91
  end
80
92
 
81
93
  def self.subdomain_or_same?(candidate, domain)
82
94
  return false unless valid?(domain) && valid?(candidate)
83
95
  return true if normalized_domain(domain) == normalized_domain(candidate)
96
+
84
97
  subdomain_of?(candidate, domain)
85
98
  end
86
99
  end
@@ -49,7 +49,7 @@ module Coppertone
49
49
  ips = recs.map do |r|
50
50
  IPAddr.new(r[:address]).mask(cidr_length.to_i)
51
51
  end
52
- ips.select { |i| !i.nil? }
52
+ ips.reject(&:nil?)
53
53
  end
54
54
  end
55
55
  end
@@ -4,7 +4,8 @@ module Coppertone
4
4
  # section 5.5 of the RFC.
5
5
  class ValidatedDomainFinder
6
6
  attr_reader :subdomain_only
7
- def initialize(macro_context, request_context, subdomain_only = true)
7
+
8
+ def initialize(macro_context, request_context, subdomain_only = true) # rubocop:disable Style/OptionalBooleanParameter
8
9
  @mc = macro_context
9
10
  @request_context = request_context
10
11
  @subdomain_only = subdomain_only
@@ -1,3 +1,3 @@
1
1
  module Coppertone
2
- VERSION = '0.0.7'.freeze
2
+ VERSION = '0.0.12'.freeze
3
3
  end
@@ -26,7 +26,7 @@ describe Coppertone::DomainSpec do
26
26
  end
27
27
 
28
28
  it 'raises an error when the macro string contains forbidden macros' do
29
- %w(c r t).each do |m|
29
+ %w[c r t].each do |m|
30
30
  expect do
31
31
  Coppertone::DomainSpec.new("%{#{m}}")
32
32
  end.to raise_error(Coppertone::DomainSpecParsingError)
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Coppertone::MacroString::MacroStaticExpand do
4
4
  context '#exists_for?' do
5
- %w(%% %_ %-).each do |x|
5
+ ['%%', '%_', '%-'].each do |x|
6
6
  it "should resolve a macro for the key #{x}" do
7
7
  expect(Coppertone::MacroString::MacroStaticExpand.exists_for?(x))
8
8
  .to eq(true)
@@ -16,7 +16,7 @@ describe Coppertone::MacroString::MacroStaticExpand do
16
16
  end
17
17
 
18
18
  context 'macro_for' do
19
- %w(%% %_ %-).each do |x|
19
+ ['%%', '%_', '%-'].each do |x|
20
20
  it "should resolve a macro for the key #{x}" do
21
21
  expect(Coppertone::MacroString::MacroStaticExpand.macro_for(x))
22
22
  .not_to be_nil
@@ -23,6 +23,7 @@ describe Coppertone::Mechanism::IP4 do
23
23
  it 'should not fail if called with an IP v6' do
24
24
  mech = Coppertone::Mechanism::IP4.new(':fe80::202:b3ff:fe1e:8329')
25
25
  expect(mech.netblock).to eq(IPAddr.new('fe80::202:b3ff:fe1e:8329'))
26
+ expect(mech.cidr_length).to eq(128)
26
27
  expect(mech.to_s).to eq('ip4:fe80::202:b3ff:fe1e:8329')
27
28
  expect(mech).not_to be_includes_ptr
28
29
  expect(mech).not_to be_context_dependent
@@ -32,6 +33,7 @@ describe Coppertone::Mechanism::IP4 do
32
33
  it 'should work if called with an IP4' do
33
34
  mech = Coppertone::Mechanism::IP4.new(':1.2.3.4')
34
35
  expect(mech.netblock).to eq(IPAddr.new('1.2.3.4'))
36
+ expect(mech.cidr_length).to eq(32)
35
37
  expect(mech.to_s).to eq('ip4:1.2.3.4')
36
38
  expect(mech).not_to be_includes_ptr
37
39
  expect(mech).not_to be_context_dependent
@@ -41,6 +43,7 @@ describe Coppertone::Mechanism::IP4 do
41
43
  it 'should work if called with an IP4 with a pfxlen' do
42
44
  mech = Coppertone::Mechanism::IP4.new(':1.2.3.4/4')
43
45
  expect(mech.netblock).to eq(IPAddr.new('1.2.3.4/4'))
46
+ expect(mech.cidr_length).to eq(4)
44
47
  expect(mech.to_s).to eq('ip4:1.2.3.4/4')
45
48
  expect(mech).not_to be_includes_ptr
46
49
  expect(mech).not_to be_context_dependent
@@ -23,6 +23,7 @@ describe Coppertone::Mechanism::IP6 do
23
23
  it 'should not fail if called with an IP v4' do
24
24
  mech = Coppertone::Mechanism::IP6.new(':1.2.3.4')
25
25
  expect(mech.netblock).to eq(IPAddr.new('1.2.3.4'))
26
+ expect(mech.cidr_length).to eq(32)
26
27
  expect(mech).not_to be_dns_lookup_term
27
28
  end
28
29
 
@@ -30,6 +31,7 @@ describe Coppertone::Mechanism::IP6 do
30
31
  mech = Coppertone::Mechanism::IP6.new(':fe80::202:b3ff:fe1e:8329')
31
32
  expect(mech.netblock)
32
33
  .to eq(IPAddr.new('fe80::202:b3ff:fe1e:8329'))
34
+ expect(mech.cidr_length).to eq(128)
33
35
  expect(mech).not_to be_dns_lookup_term
34
36
  end
35
37
 
@@ -37,6 +39,7 @@ describe Coppertone::Mechanism::IP6 do
37
39
  mech = Coppertone::Mechanism::IP6.new(':fe80::202:b3ff:fe1e:8329/64')
38
40
  expect(mech.netblock)
39
41
  .to eq(IPAddr.new('fe80::202:b3ff:fe1e:8329/64'))
42
+ expect(mech.cidr_length).to eq(64)
40
43
  expect(mech).not_to be_dns_lookup_term
41
44
  end
42
45
 
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Coppertone::NullMacroContext do
4
- %w(s l o d i p v h c r t).each do |i|
4
+ %w[s l o d i p v h c r t].each do |i|
5
5
  it "should raise an error for #{i}" do
6
6
  expect do
7
7
  Coppertone::NullMacroContext::NULL_CONTEXT.send(i)
@@ -1,5 +1,3 @@
1
- # -- encoding : utf-8 --
2
-
3
1
  require 'spec_helper'
4
2
 
5
3
  describe 'Initial processing' do
@@ -1,5 +1,3 @@
1
- # -- encoding : utf-8 --
2
-
3
1
  require 'spec_helper'
4
2
 
5
3
  describe 'Semantics of exp and other modifiers' do
data/spec/record_spec.rb CHANGED
@@ -141,7 +141,7 @@ describe Coppertone::Record do
141
141
  .new('v=spf1 include:_spf.domain1.com ip4:1.2.3.4 include:_spf.domain2.com ~all exp=explain._spf.%{d}')
142
142
  includes = r.includes
143
143
  expect(includes.map(&:mechanism).all? { |i| i.is_a?(Coppertone::Mechanism::Include) }).to be_truthy
144
- expect(includes.map(&:target_domain)).to eq(%w(_spf.domain1.com _spf.domain2.com))
144
+ expect(includes.map(&:target_domain)).to eq(%w[_spf.domain1.com _spf.domain2.com])
145
145
  end
146
146
  end
147
147
 
data/spec/spec_helper.rb CHANGED
@@ -1,8 +1,19 @@
1
1
  require 'simplecov'
2
- require 'codeclimate-test-reporter'
3
- CodeClimate::TestReporter.start || SimpleCov.start
2
+
3
+ if ENV['CI'] == 'true'
4
+ require 'codecov'
5
+ SimpleCov.formatter = SimpleCov::Formatter::Codecov
6
+ end
7
+
8
+ # save to CircleCI's artifacts directory if we're on CircleCI
9
+ if ENV['CIRCLE_ARTIFACTS']
10
+ dir = File.join(ENV['CIRCLE_ARTIFACTS'], 'coverage')
11
+ SimpleCov.coverage_dir(dir)
12
+ end
13
+
14
+ SimpleCov.start
4
15
 
5
16
  require 'coppertone'
6
17
 
7
18
  # Support files
8
- Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
19
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f }