dmarc 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3b3ddde1f4d2db595acab01a63352cb7a62e9cdf
4
- data.tar.gz: 8199be76eb510e8693dc45fe296227ec4a00f9e4
3
+ metadata.gz: da372737bb262f1d63188a9bfac317660bb8cdb4
4
+ data.tar.gz: d29823cc84b2e12a8aa6c37d4cfe0c1c0ad95006
5
5
  SHA512:
6
- metadata.gz: 0a1762477121db2b245d80c5929432850e68727043d019bd272a5fabbe26d766005e25bbc28e872fcfafcd03b7b1e18ec899d8de447b7630310e69cbaab8ae9a
7
- data.tar.gz: aaf8c594a50a80c593976dad9d9d532e3abbae5fa7e1d42aa0c2743810588e16e156496650872698cecad50959f7c2b9b22f67e238ee7a56cafff7400a82d9fd
6
+ metadata.gz: 938905706d15411759c04133451472d0d7d9565982240ed9747e1566b2c450cab830b66ae6edffbfe8e193ea91bd1aae0dab655a4b6c453b181755f549930e21
7
+ data.tar.gz: 764c5540136f70d7d7abd04a5b28253473c8288d82f934f1ad2f57f93d1bbc85ad832277d967957b568f8a27615007ce472a1d09a002cc7ab6c6bd2523145c41
data/.document ADDED
@@ -0,0 +1,3 @@
1
+ -
2
+ ChangeLog.md
3
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - jruby-19mode
7
+ - rbx-2
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown --title "DMARC Documentation" --protected
data/ChangeLog.md ADDED
@@ -0,0 +1,12 @@
1
+ ### 0.2.0 / 2014-10-20
2
+
3
+ * Added {DMARC::Error}.
4
+ * Added {DMARC::InvalidRecord}.
5
+ * Add support for parsing `fo` tokens.
6
+ * Ignore unknown tags instead of raising a parser exception.
7
+ * Ignore tags with invalid values instead of raising a parser exception.
8
+
9
+ ### 0.1.0 / 2014-04-25
10
+
11
+ * Initial release.
12
+
data/Gemfile CHANGED
@@ -1,3 +1,12 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+ group :development do
6
+ gem 'rake'
7
+ gem 'nokogiri'
8
+ gem 'rspec', '~> 3.0'
9
+
10
+ gem 'kramdown'
11
+ gem 'yard', '~> 0.8'
12
+ end
File without changes
data/README.md CHANGED
@@ -1,17 +1,41 @@
1
- dmarc
2
- =====
1
+ # DMARC
3
2
 
4
- [DMARC](http://tools.ietf.org/html/draft-kucherawy-dmarc-base-02) is a
5
- technical specification intended to solve a couple of long-standing email
6
- authentication problems. DMARC policies are described in DMARC "records," which
7
- are stored as DNS TXT records on a subdomain. This library contains a parser
8
- for DMARC records.
3
+ [![Code Climate](https://codeclimate.com/github/trailofbits/dmarc.png)](https://codeclimate.com/github/trailofbits/dmarc) [![Build Status](https://travis-ci.org/trailofbits/dmarc.svg)](https://travis-ci.org/trailofbits/dmarc)
4
+ [![Gem Version](https://badge.fury.io/rb/dmarc.svg)](http://badge.fury.io/rb/dmarc)
5
+ [![YARD Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/gems/dmarc)
9
6
 
10
- Usage
11
- -----
7
+ [DMARC] is a technical specification intended to solve a couple of long-standing
8
+ email authentication problems. DMARC policies are described in DMARC "records,"
9
+ which are stored as DNS TXT records on a subdomain. This library contains a
10
+ parser for DMARC records.
12
11
 
13
- ```ruby
14
- require 'dmarc/record'
15
- record = DMARC::Record.from_txt(txt) # txt is a DNS TXT record containing the DMARC policy
16
- ```
12
+ ## Example
17
13
 
14
+ require 'dmarc'
15
+
16
+ record = DMARC::Record.from_txt(txt)
17
+
18
+ ## Requirements
19
+
20
+ * [parslet] ~> 1.5
21
+
22
+ ## Install
23
+
24
+ $ gem install dmarc
25
+
26
+ ## Testing
27
+
28
+ To run the RSpec tests:
29
+
30
+ $ rake spec
31
+
32
+ To test the parser against the Alexa Top 500:
33
+
34
+ $ rake spec:gauntlet
35
+
36
+ ## License
37
+
38
+ See the {file:LICENSE.txt} file.
39
+
40
+ [DMARC]: http://tools.ietf.org/html/draft-kucherawy-dmarc-base-02
41
+ [parslet]: http://kschiess.github.io/parslet/
data/Rakefile CHANGED
@@ -16,9 +16,18 @@ require 'bundler/gem_tasks'
16
16
  require 'rspec/core/rake_task'
17
17
  RSpec::Core::RakeTask.new
18
18
 
19
+ namespace :spec do
20
+ desc "Tests DMARC::Parser against Alexa Top 500"
21
+ RSpec::Core::RakeTask.new(:gauntlet) do |t|
22
+ t.rspec_opts = '--tag gauntlet'
23
+ end
24
+ end
25
+
19
26
  task :test => :spec
20
27
  task :default => :spec
21
28
 
22
29
  require 'yard'
23
30
  YARD::Rake::YardocTask.new
24
31
  task :doc => :yard
32
+
33
+ require_relative 'tasks/alexa'
data/dmarc.gemspec CHANGED
@@ -20,7 +20,4 @@ Gem::Specification.new do |gem|
20
20
  gem.add_dependency 'parslet', '~> 1.5'
21
21
 
22
22
  gem.add_development_dependency 'bundler', '~> 1.0'
23
- gem.add_development_dependency 'rake', '~> 10.0'
24
- gem.add_development_dependency 'rspec', '~> 2.8'
25
- gem.add_development_dependency 'yard', '~> 0.8'
26
23
  end
data/lib/dmarc.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'dmarc/exceptions'
1
2
  require 'dmarc/record'
2
3
  require 'dmarc/parser'
3
4
  require 'dmarc/version'
@@ -0,0 +1,19 @@
1
+ module DMARC
2
+ class Error < StandardError; end
3
+ class InvalidRecord < Error
4
+ attr_reader :original
5
+
6
+ def initialize(msg = nil, original = $!)
7
+ super msg
8
+ @original = original
9
+ end
10
+
11
+ def ascii_tree
12
+ # `cause` is a method defined by parslet on the ParseFailed error
13
+ # Not to be confused with ruby 2.1's Exception#cause method
14
+ if self.original != nil
15
+ self.original.cause.ascii_tree
16
+ end
17
+ end
18
+ end
19
+ end
data/lib/dmarc/parser.rb CHANGED
@@ -3,175 +3,197 @@ require 'parslet'
3
3
  module DMARC
4
4
  class Parser < Parslet::Parser
5
5
 
6
- root('dmarc_record')
7
- rule('dmarc_record') do
8
- dmarc_version >>
9
- (dmarc_sep >> (
10
- dmarc_request |
11
- dmarc_srequest |
12
- dmarc_auri |
13
- dmarc_furi |
14
- dmarc_adkim |
15
- dmarc_aspf |
16
- dmarc_ainterval |
17
- dmarc_rfmt |
18
- dmarc_percent
19
- )).repeat >>
6
+ root :dmarc_record
7
+
8
+ rule(:dmarc_record) do
9
+ dmarc_version.repeat(1,1) >> dmarc_sep >>
10
+ dmarc_request.maybe >>
11
+ (dmarc_sep >> dmarc_tag).repeat >>
20
12
  dmarc_sep.maybe
21
13
  end
22
14
 
23
- rule('dmarc_version') do
24
- str('v') >> wsp.repeat >>
25
- str('=') >> wsp.repeat >>
15
+ rule(:dmarc_sep) { wsp? >> str(';') >> wsp? }
16
+
17
+ rule(:dmarc_version) do
18
+ str('v') >> wsp? >>
19
+ str('=') >> wsp? >>
26
20
  str('DMARC1').as(:v)
27
21
  end
28
- rule('dmarc_sep') { wsp.repeat >> str(';') >> wsp.repeat }
29
22
 
30
- rule('dmarc_request') do
31
- str('p') >> wsp.repeat >> str('=') >> wsp.repeat >> (
23
+ rule(:dmarc_request) do
24
+ str('p') >> wsp? >> str('=') >> wsp? >> (
32
25
  str('none') |
33
26
  str('quarantine') |
34
27
  str('reject')
35
28
  ).as(:p)
36
29
  end
37
30
 
38
- rule('dmarc_srequest') do
39
- str('sp') >> wsp.repeat >> str('=') >> wsp.repeat >> (
40
- str('none') |
41
- str('quarantine') |
42
- str('reject')
43
- ).as(:sp)
31
+ rule(:dmarc_tag) do
32
+ dmarc_srequest |
33
+ dmarc_auri |
34
+ dmarc_furi |
35
+ dmarc_adkim |
36
+ dmarc_aspf |
37
+ dmarc_ainterval |
38
+ dmarc_fo |
39
+ dmarc_rfmt |
40
+ dmarc_percent |
41
+ unknown_tag
44
42
  end
45
43
 
46
- rule('dmarc_auri') do
47
- str('rua') >> wsp.repeat >> str('=') >> wsp.repeat >>
48
- dmarc_uri.as(:rua) >> (
49
- wsp.repeat >> str(',') >> wsp.repeat >> dmarc_uri.as(:rua)
50
- ).repeat
44
+ def self.tag_rule(name,tag,&block)
45
+ rule(:"dmarc_#{name}") do
46
+ str(tag) >> wsp? >> str('=') >> wsp? >>
47
+ (instance_eval(&block).as(tag.to_sym) | unknown_value)
48
+ end
51
49
  end
52
50
 
53
- rule('dmarc_ainterval') do
54
- str('ri') >> wsp.repeat >> str('=') >> wsp.repeat >> digit.repeat(1).as(:ri)
51
+ tag_rule(:srequest,'sp') do
52
+ str('none') | str('quarantine') | str('reject')
55
53
  end
56
54
 
57
- rule('dmarc_furi') do
58
- str('ruf') >> wsp.repeat >> str('=') >> wsp.repeat >>
59
- dmarc_uri.as(:ruf) >> (wsp.repeat >> str(',') >> wsp.repeat >> dmarc_uri.as(:ruf)).repeat
55
+ tag_rule(:auri, 'rua') do
56
+ dmarc_uri >> (wsp? >> str(',') >> wsp? >> dmarc_uri).repeat
60
57
  end
61
58
 
62
- rule('dmarc_rfmt') do
63
- str('rf') >> wsp.repeat >> str('=') >> wsp.repeat >> (
64
- str('afrf') |
65
- str('iodef')
66
- ).as(:rf)
67
- end
59
+ tag_rule(:ainterval,'ri') { digit.repeat(1) }
68
60
 
69
- rule('dmarc_percent') do
70
- str('pct') >> wsp.repeat >> str('=') >> wsp.repeat >> digit.repeat(1, 3).as(:pct)
61
+ tag_rule(:furi,'ruf') do
62
+ dmarc_uri >> (wsp? >> str(',') >> wsp? >> dmarc_uri).repeat
71
63
  end
72
64
 
73
- rule('dmarc_adkim') do
74
- str('adkim') >> wsp.repeat >> str('=') >> wsp.repeat >> (
75
- str('r') |
76
- str('s')
77
- ).as(:adkim)
65
+ tag_rule(:fo,'fo') do
66
+ fo_opt >> (wsp? >> str(':') >> wsp? >> fo_opt).repeat
78
67
  end
79
68
 
80
- rule('dmarc_aspf') do
81
- str('aspf') >> wsp.repeat >> str('=') >> wsp.repeat >> (
82
- str('r') |
83
- str('s')
84
- ).as(:aspf)
85
- end
69
+ rule(:fo_opt) { match['01ds'].as(:fo_opt) }
70
+
71
+ tag_rule(:rfmt,'rf') { str('afrf') | str('iodef') }
72
+
73
+ tag_rule(:percent,'pct') { digit.repeat(1,3) }
86
74
 
87
- rule('dmarc_uri') do
75
+ tag_rule(:adkim, 'adkim') { match['rs'] }
76
+ tag_rule(:aspf, 'aspf') { match['rs'] }
77
+
78
+ rule(:unknown_tag) { match["^; \t"].repeat(1) }
79
+ rule(:unknown_value) { match["^=; \t"].repeat(1) }
80
+
81
+ rule(:dmarc_uri) do
88
82
  uri.as(:uri) >> (
89
83
  str('!') >> digit.repeat(1).as(:size) >> (
90
- str('k') |
91
- str('m') |
92
- str('g') |
93
- str('t')
84
+ match['kmgt']
94
85
  ).as(:unit).maybe
95
86
  ).maybe
96
87
  end
97
88
 
98
- rule('uri') do
89
+ rule(:uri) do
99
90
  ( absoluteURI | relativeURI ).maybe >>
100
91
  ( str('#') >> fragment ).maybe
101
92
  end
102
- rule('absoluteURI') { scheme >> str(':') >> ( hier_part | opaque_part ) }
103
- rule('relativeURI') do
93
+ rule(:absoluteURI) { scheme >> str(':') >> ( hier_part | opaque_part ) }
94
+ rule(:relativeURI) do
104
95
  ( net_path | abs_path | rel_path ) >> ( str('?') >> query ).maybe
105
96
  end
106
97
 
107
- rule('hier_part') do
98
+ rule(:hier_part) do
108
99
  ( net_path | abs_path ) >> ( str('?') >> query )
109
100
  end
110
- rule('opaque_part') do
101
+ rule(:opaque_part) do
111
102
  uric_no_slash >> uric.repeat
112
103
  end
113
104
 
114
- rule('uric_no_slash') do
105
+ rule(:uric_no_slash) do
115
106
  unreserved | escaped | match('[?:@&=+$]')
116
107
  end
117
108
 
118
- rule('net_path') { str('//') >> authority >> abs_path.maybe }
119
- rule('abs_path') { str('/') >> path_segments }
120
- rule('rel_path') { rel_segment >> abs_path.maybe }
109
+ rule(:net_path) { str('//') >> authority >> abs_path.maybe }
110
+ rule(:abs_path) { str('/') >> path_segments }
111
+ rule(:rel_path) { rel_segment >> abs_path.maybe }
121
112
 
122
- rule('rel_segment') { ( unreserved | escaped | match('[@&=+$]') ).repeat(1) }
113
+ rule(:rel_segment) { ( unreserved | escaped | match('[@&=+$]') ).repeat(1) }
123
114
 
124
- rule('scheme') { alpha >> ( alpha | digit | match('[+-.]') ).repeat }
115
+ rule(:scheme) { alpha >> ( alpha | digit | match('[+-.]') ).repeat }
125
116
 
126
- rule('authority') { server | reg_name }
117
+ rule(:authority) { server | reg_name }
127
118
 
128
- rule('reg_name') { ( unreserved | escaped | match('[$:@&=+]') ).repeat(1) }
119
+ rule(:reg_name) { ( unreserved | escaped | match('[$:@&=+]') ).repeat(1) }
129
120
 
130
- rule('server') { ( ( userinfo >> str('@') ).maybe >> hostport ).maybe }
131
- rule('userinfo') { ( unreserved | escaped | match('[:&=+$]') ).repeat }
121
+ rule(:server) { ( ( userinfo >> str('@') ).maybe >> hostport ).maybe }
122
+ rule(:userinfo) { ( unreserved | escaped | match('[:&=+$]') ).repeat }
132
123
 
133
- rule('hostport') { host >> ( str(':') >> port ).maybe }
134
- rule('host') { hostname | ipv4address }
135
- rule('hostname') do
124
+ rule(:hostport) { host >> ( str(':') >> port ).maybe }
125
+ rule(:host) { hostname | ipv4address }
126
+ rule(:hostname) do
136
127
  ( domainlabel >> str('.') ).repeat >> toplabel >> str('.').maybe
137
128
  end
138
- rule('domainlabel') do
129
+ rule(:domainlabel) do
139
130
  alphanum | (
140
131
  alphanum >> ( alphanum | str('-') ).repeat >> alphanum
141
132
  )
142
133
  end
143
- rule('toplabel') do
134
+ rule(:toplabel) do
144
135
  alpha | (
145
136
  alpha >> ( alphanum | str('-') ).repeat >> alphanum
146
137
  )
147
138
  end
148
- rule('ipv4address') do
139
+ rule(:ipv4address) do
149
140
  digit.repeat(1) >> str('.') >>
150
141
  digit.repeat(1) >> str('.') >>
151
142
  digit.repeat(1) >> str('.') >>
152
143
  digit.repeat(1)
153
144
  end
154
- rule('port') { digit.repeat }
155
-
156
- rule('path') { ( abs_path | opaque_part ).maybe }
157
- rule('path_segments') { segment >> ( str('/') >> segment ).repeat }
158
- rule('segment') { pchar.repeat >> ( str(';') >> param ).repeat }
159
- rule('param') { pchar }
160
- rule('pchar') { unreserved | escaped | match('[:@&=+$]') }
161
-
162
- rule('query') { uric.repeat }
163
- rule('fragment') { uric.repeat }
164
-
165
- rule('uric') { reserved | unreserved | escaped }
166
- rule('reserved') { match('[/?:@&=+$]') }
167
- rule('unreserved') { alphanum | mark }
168
- rule('mark') { match("[-_.~*'()]") }
169
- rule('escaped') { str('%') >> hex >> hex }
170
- rule('hex') { digit | match('[a-fA-F]') }
171
- rule('alphanum') { alpha | digit }
172
- rule('alpha') { match('[a-zA-Z]') }
173
- rule('digit') { match('[0-9]') }
174
- rule('wsp') { str(' ') | str("\t") }
145
+ rule(:port) { digit.repeat }
146
+
147
+ rule(:path) { ( abs_path | opaque_part ).maybe }
148
+ rule(:path_segments) { segment >> ( str('/') >> segment ).repeat }
149
+ rule(:segment) { pchar.repeat >> ( str(';') >> param ).repeat }
150
+ rule(:param) { pchar }
151
+ rule(:pchar) { unreserved | escaped | match('[:@&=+$]') }
152
+
153
+ rule(:query) { uric.repeat }
154
+ rule(:fragment) { uric.repeat }
155
+
156
+ rule(:uric) { reserved | unreserved | escaped }
157
+ rule(:reserved) { match('[/?:@&=+$]') }
158
+ rule(:unreserved) { alphanum | mark }
159
+ rule(:mark) { match("[-_.~*'()]") }
160
+ rule(:escaped) { str('%') >> hex >> hex }
161
+ rule(:hex) { digit | match('[a-fA-F]') }
162
+ rule(:alphanum) { alpha | digit }
163
+ rule(:alpha) { match('[a-zA-Z]') }
164
+ rule(:digit) { match('[0-9]') }
165
+ rule(:wsp) { str(' ') | str("\t") }
166
+ rule(:wsp?) { wsp.repeat }
167
+
168
+ class Transform < Parslet::Transform
169
+
170
+ rule(:fo_opt => simple(:fo_opt)) { fo_opt }
171
+
172
+ rule(:p => simple(:p)) { {p: p.to_sym } }
173
+ rule(:sp => simple(:sp)) { {sp: sp.to_sym} }
174
+
175
+ rule(:pct => simple(:pct)) { {pct: pct.to_i} }
176
+ rule(:ri => simple(:ri)) { {ri: ri.to_i} }
177
+
178
+ end
179
+
180
+ #
181
+ # Parses a DMARC record.
182
+ #
183
+ # @param [String] record
184
+ # The raw DMARC record to parse.
185
+ #
186
+ # @return [Hash{Symbol => Object}]
187
+ # The Hash of tags within the record.
188
+ #
189
+ def parse(record)
190
+ tags = Transform.new.apply(super(record))
191
+ hash = {}
192
+
193
+ tags.each { |tag| hash.merge!(tag) }
194
+
195
+ return hash
196
+ end
175
197
 
176
198
  end
177
199
  end