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 +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
|
+
[![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
|
-
|
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
|