dmarc 0.1.0 → 0.2.0

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