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 +4 -4
- data/.document +3 -0
- data/.rspec +1 -0
- data/.travis.yml +7 -0
- data/.yardopts +1 -0
- data/ChangeLog.md +12 -0
- data/Gemfile +9 -0
- data/{LICENSE → LICENSE.txt} +0 -0
- data/README.md +37 -13
- data/Rakefile +9 -0
- data/dmarc.gemspec +0 -3
- data/lib/dmarc.rb +1 -0
- data/lib/dmarc/exceptions.rb +19 -0
- data/lib/dmarc/parser.rb +124 -102
- data/lib/dmarc/record.rb +7 -9
- data/lib/dmarc/version.rb +1 -1
- data/spec/data/alexa.csv +500 -0
- data/spec/{lib/dmarc/parser_spec.rb → parser_spec.rb} +144 -17
- data/spec/{lib/dmarc/record_spec.rb → record_spec.rb} +27 -7
- data/spec/spec_helper.rb +8 -0
- data/tasks/alexa.rb +43 -0
- metadata +19 -50
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: da372737bb262f1d63188a9bfac317660bb8cdb4
|
4
|
+
data.tar.gz: d29823cc84b2e12a8aa6c37d4cfe0c1c0ad95006
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 938905706d15411759c04133451472d0d7d9565982240ed9747e1566b2c450cab830b66ae6edffbfe8e193ea91bd1aae0dab655a4b6c453b181755f549930e21
|
7
|
+
data.tar.gz: 764c5540136f70d7d7abd04a5b28253473c8288d82f934f1ad2f57f93d1bbc85ad832277d967957b568f8a27615007ce472a1d09a002cc7ab6c6bd2523145c41
|
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour --format documentation
|
data/.travis.yml
ADDED
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
data/{LICENSE → LICENSE.txt}
RENAMED
File without changes
|
data/README.md
CHANGED
@@ -1,17 +1,41 @@
|
|
1
|
-
|
2
|
-
=====
|
1
|
+
# DMARC
|
3
2
|
|
4
|
-
[
|
5
|
-
|
6
|
-
|
7
|
-
are stored as DNS TXT records on a subdomain. This library contains a parser
|
8
|
-
for DMARC records.
|
3
|
+
[](https://codeclimate.com/github/trailofbits/dmarc) [](https://travis-ci.org/trailofbits/dmarc)
|
4
|
+
[](http://badge.fury.io/rb/dmarc)
|
5
|
+
[](http://rubydoc.info/gems/dmarc)
|
9
6
|
|
10
|
-
|
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
|
-
|
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
@@ -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
|
7
|
-
|
8
|
-
|
9
|
-
(dmarc_sep >>
|
10
|
-
|
11
|
-
|
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('
|
24
|
-
|
25
|
-
|
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(
|
31
|
-
str('p') >> wsp
|
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(
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
54
|
-
str('
|
51
|
+
tag_rule(:srequest,'sp') do
|
52
|
+
str('none') | str('quarantine') | str('reject')
|
55
53
|
end
|
56
54
|
|
57
|
-
|
58
|
-
|
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
|
-
|
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
|
-
|
70
|
-
|
61
|
+
tag_rule(:furi,'ruf') do
|
62
|
+
dmarc_uri >> (wsp? >> str(',') >> wsp? >> dmarc_uri).repeat
|
71
63
|
end
|
72
64
|
|
73
|
-
|
74
|
-
|
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('
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
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
|
-
|
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(
|
89
|
+
rule(:uri) do
|
99
90
|
( absoluteURI | relativeURI ).maybe >>
|
100
91
|
( str('#') >> fragment ).maybe
|
101
92
|
end
|
102
|
-
rule(
|
103
|
-
rule(
|
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(
|
98
|
+
rule(:hier_part) do
|
108
99
|
( net_path | abs_path ) >> ( str('?') >> query )
|
109
100
|
end
|
110
|
-
rule(
|
101
|
+
rule(:opaque_part) do
|
111
102
|
uric_no_slash >> uric.repeat
|
112
103
|
end
|
113
104
|
|
114
|
-
rule(
|
105
|
+
rule(:uric_no_slash) do
|
115
106
|
unreserved | escaped | match('[?:@&=+$]')
|
116
107
|
end
|
117
108
|
|
118
|
-
rule(
|
119
|
-
rule(
|
120
|
-
rule(
|
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(
|
113
|
+
rule(:rel_segment) { ( unreserved | escaped | match('[@&=+$]') ).repeat(1) }
|
123
114
|
|
124
|
-
rule(
|
115
|
+
rule(:scheme) { alpha >> ( alpha | digit | match('[+-.]') ).repeat }
|
125
116
|
|
126
|
-
rule(
|
117
|
+
rule(:authority) { server | reg_name }
|
127
118
|
|
128
|
-
rule(
|
119
|
+
rule(:reg_name) { ( unreserved | escaped | match('[$:@&=+]') ).repeat(1) }
|
129
120
|
|
130
|
-
rule(
|
131
|
-
rule(
|
121
|
+
rule(:server) { ( ( userinfo >> str('@') ).maybe >> hostport ).maybe }
|
122
|
+
rule(:userinfo) { ( unreserved | escaped | match('[:&=+$]') ).repeat }
|
132
123
|
|
133
|
-
rule(
|
134
|
-
rule(
|
135
|
-
rule(
|
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(
|
129
|
+
rule(:domainlabel) do
|
139
130
|
alphanum | (
|
140
131
|
alphanum >> ( alphanum | str('-') ).repeat >> alphanum
|
141
132
|
)
|
142
133
|
end
|
143
|
-
rule(
|
134
|
+
rule(:toplabel) do
|
144
135
|
alpha | (
|
145
136
|
alpha >> ( alphanum | str('-') ).repeat >> alphanum
|
146
137
|
)
|
147
138
|
end
|
148
|
-
rule(
|
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(
|
155
|
-
|
156
|
-
rule(
|
157
|
-
rule(
|
158
|
-
rule(
|
159
|
-
rule(
|
160
|
-
rule(
|
161
|
-
|
162
|
-
rule(
|
163
|
-
rule(
|
164
|
-
|
165
|
-
rule(
|
166
|
-
rule(
|
167
|
-
rule(
|
168
|
-
rule(
|
169
|
-
rule(
|
170
|
-
rule(
|
171
|
-
rule(
|
172
|
-
rule(
|
173
|
-
rule(
|
174
|
-
rule(
|
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
|