dmarc 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +15 -5
- data/ChangeLog.md +13 -1
- data/Gemfile +5 -0
- data/README.md +43 -2
- data/dmarc.gemspec +1 -0
- data/lib/dmarc.rb +1 -0
- data/lib/dmarc/dmarc.rb +30 -0
- data/lib/dmarc/exceptions.rb +3 -16
- data/lib/dmarc/parser.rb +29 -17
- data/lib/dmarc/record.rb +190 -17
- data/lib/dmarc/version.rb +1 -1
- data/spec/dmarc_spec.rb +24 -0
- data/spec/parser_spec.rb +285 -239
- data/spec/record_spec.rb +91 -35
- data/spec/spec_helper.rb +3 -0
- metadata +18 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9a12630bf22487064d3d35386f55e7af00f6b542
|
4
|
+
data.tar.gz: 35f68803f6820445c7e38d9e37078737075807e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3214787e24b80ce2eb162473d43e9e77f152d8e1ff6cfcf1fbbb2fadfe8d565b27c4e0b97c3c17180783e10ff6c62a18714c8be3925a97309a886f79a7d2eee4
|
7
|
+
data.tar.gz: f7ed4b603e8541e97c7d29d733f45d5b7f93ee1d737f9cc70916c1459098a570a01e1cdece1b2b182dd683c8b2322d279ed4db2c3fbe5187ec8be9eb2389d7fe
|
data/.travis.yml
CHANGED
@@ -1,7 +1,17 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm:
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
- 1.9.3
|
4
|
+
- 2.0
|
5
|
+
- 2.1
|
6
|
+
- 2.2
|
7
|
+
- jruby
|
8
|
+
- rbx-2
|
9
|
+
matrix:
|
10
|
+
allow_failures:
|
11
|
+
- rvm: rbx-2
|
12
|
+
addons:
|
13
|
+
code_climate:
|
14
|
+
repo_token: 27519af871c1d9576b91e6f264e841eb7b7babbdcda315bf94fcbd095c202949
|
15
|
+
notifications:
|
16
|
+
slack:
|
17
|
+
secure: Jqj30lwH2fnyu8adMI2nXa7rfF70o5JSrL0ZOe425XVu3YMMpfIp8J4DD0Ks/nCxgGRhJpq35oAHXjKmJAEHyda9oxx7GV9QwOuu2oviUrW3VUQQj9UsXPQP0sc0GD9BPI+KVNp4dbGki1ik90I6HVVsqDauek/z5MWt6MJY+tA=
|
data/ChangeLog.md
CHANGED
@@ -1,6 +1,18 @@
|
|
1
|
+
### 0.3.0 / 2015-07-01
|
2
|
+
|
3
|
+
* Added {DMARC::Record.query}.
|
4
|
+
* Added {DMARC::Record#to_s}.
|
5
|
+
* Added {DMARC::InvalidRecord}.
|
6
|
+
* Deprecate {DMARC::Record.from_txt}.
|
7
|
+
* {DMARC::Record#v} now returns `:DMARC1`.
|
8
|
+
* {DMARC::Record#p} and {DMARC::Record#sp} now return Symbols
|
9
|
+
* {DMARC::Record#rua} and {DMARC::Record#ruf} will always return Arrays.
|
10
|
+
* Fixed a bug in {DMARC::Parser} with respect to order of tags.
|
11
|
+
* {DMARC::Parser::Transform} now coerces URIs into URI objects.
|
12
|
+
|
1
13
|
### 0.2.0 / 2014-10-20
|
2
14
|
|
3
|
-
* Added
|
15
|
+
* Added `DMARC::Error`.
|
4
16
|
* Added {DMARC::InvalidRecord}.
|
5
17
|
* Add support for parsing `fo` tokens.
|
6
18
|
* Ignore unknown tags instead of raising a parser exception.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -3,6 +3,7 @@
|
|
3
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
4
|
[![Gem Version](https://badge.fury.io/rb/dmarc.svg)](http://badge.fury.io/rb/dmarc)
|
5
5
|
[![YARD Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/gems/dmarc)
|
6
|
+
[![Test Coverage](https://codeclimate.com/github/trailofbits/dmarc/badges/coverage.svg)](https://codeclimate.com/github/trailofbits/dmarc)
|
6
7
|
|
7
8
|
[DMARC] is a technical specification intended to solve a couple of long-standing
|
8
9
|
email authentication problems. DMARC policies are described in DMARC "records,"
|
@@ -11,9 +12,49 @@ parser for DMARC records.
|
|
11
12
|
|
12
13
|
## Example
|
13
14
|
|
15
|
+
Parse a SPF record:
|
16
|
+
|
14
17
|
require 'dmarc'
|
15
18
|
|
16
|
-
record = DMARC::Record.
|
19
|
+
record = DMARC::Record.parse("v=DMARC1; p=reject; rua=mailto:d@rua.agari.com; ruf=mailto:d@ruf.agari.com; fo=1")
|
20
|
+
|
21
|
+
record.v
|
22
|
+
# => :DMARC1
|
23
|
+
|
24
|
+
record.adkim
|
25
|
+
# => :r
|
26
|
+
|
27
|
+
record.aspf
|
28
|
+
# => :r
|
29
|
+
|
30
|
+
record.fo
|
31
|
+
# => ["0"]
|
32
|
+
|
33
|
+
record.p
|
34
|
+
# => :reject
|
35
|
+
|
36
|
+
record.pct
|
37
|
+
# => 100
|
38
|
+
|
39
|
+
record.rf
|
40
|
+
# => :afrf
|
41
|
+
|
42
|
+
record.ri
|
43
|
+
# => 86400
|
44
|
+
|
45
|
+
record.rua
|
46
|
+
# => [#<URI::MailTo:0x000000034a1cc8 URL:mailto:d@rua.agari.com>]
|
47
|
+
|
48
|
+
record.ruf
|
49
|
+
# => [#<URI::MailTo:0x000000034a02b0 URL:mailto:d@ruf.agari.com>]
|
50
|
+
|
51
|
+
record.sp
|
52
|
+
# => :reject
|
53
|
+
|
54
|
+
Query the SPF record for a domain:
|
55
|
+
|
56
|
+
record = DMARC::Record.query('twitter.com')
|
57
|
+
# => #<DMARC::Record:0x0000000313bd90 @adkim=:r, @aspf=:r, @fo=["1"@79], @p=:reject, @pct=100, @rf=:afrf, @ri=86400, @rua=[#<URI::MailTo:0x00000003124e38 URL:mailto:d@rua.agari.com>], @ruf=[#<URI::MailTo:0x00000003132678 URL:mailto:d@ruf.agari.com>], @sp=:reject, @v=:DMARC1>
|
17
58
|
|
18
59
|
## Requirements
|
19
60
|
|
@@ -37,5 +78,5 @@ To test the parser against the Alexa Top 500:
|
|
37
78
|
|
38
79
|
See the {file:LICENSE.txt} file.
|
39
80
|
|
40
|
-
[DMARC]:
|
81
|
+
[DMARC]: https://tools.ietf.org/html/rfc7489
|
41
82
|
[parslet]: http://kschiess.github.io/parslet/
|
data/dmarc.gemspec
CHANGED
data/lib/dmarc.rb
CHANGED
data/lib/dmarc/dmarc.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'resolv'
|
2
|
+
|
3
|
+
module DMARC
|
4
|
+
#
|
5
|
+
# Queries a domain for the DMARC record.
|
6
|
+
#
|
7
|
+
# @param [String] domain
|
8
|
+
# The domain to query DMARC for.
|
9
|
+
#
|
10
|
+
# @param [Resolv::DNS] resolver
|
11
|
+
# The resolver to use.
|
12
|
+
#
|
13
|
+
# @return [String, nil]
|
14
|
+
# The domain's DMARC record or `nil` if none exists.
|
15
|
+
#
|
16
|
+
# @api semipublic
|
17
|
+
#
|
18
|
+
# @since 0.3.0
|
19
|
+
#
|
20
|
+
def self.query(domain,resolver=Resolv::DNS.new)
|
21
|
+
host = "_dmarc.#{domain}"
|
22
|
+
|
23
|
+
begin
|
24
|
+
return resolver.getresource(
|
25
|
+
host, Resolv::DNS::Resource::IN::TXT
|
26
|
+
).strings.join
|
27
|
+
rescue Resolv::ResolvError
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/dmarc/exceptions.rb
CHANGED
@@ -1,19 +1,6 @@
|
|
1
|
-
|
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
|
1
|
+
require 'parslet'
|
10
2
|
|
11
|
-
|
12
|
-
|
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
|
3
|
+
module DMARC
|
4
|
+
class InvalidRecord < Parslet::ParseFailed
|
18
5
|
end
|
19
6
|
end
|
data/lib/dmarc/parser.rb
CHANGED
@@ -1,13 +1,19 @@
|
|
1
1
|
require 'parslet'
|
2
2
|
|
3
|
+
require 'uri'
|
4
|
+
|
3
5
|
module DMARC
|
6
|
+
#
|
7
|
+
# DMARC parser.
|
8
|
+
#
|
9
|
+
# @see https://tools.ietf.org/html/rfc7489#section-6.4
|
10
|
+
#
|
4
11
|
class Parser < Parslet::Parser
|
5
12
|
|
6
13
|
root :dmarc_record
|
7
14
|
|
8
15
|
rule(:dmarc_record) do
|
9
|
-
dmarc_version.repeat(1,1) >>
|
10
|
-
dmarc_request.maybe >>
|
16
|
+
dmarc_version.repeat(1,1) >>
|
11
17
|
(dmarc_sep >> dmarc_tag).repeat >>
|
12
18
|
dmarc_sep.maybe
|
13
19
|
end
|
@@ -20,15 +26,8 @@ module DMARC
|
|
20
26
|
str('DMARC1').as(:v)
|
21
27
|
end
|
22
28
|
|
23
|
-
rule(:dmarc_request) do
|
24
|
-
str('p') >> wsp? >> str('=') >> wsp? >> (
|
25
|
-
str('none') |
|
26
|
-
str('quarantine') |
|
27
|
-
str('reject')
|
28
|
-
).as(:p)
|
29
|
-
end
|
30
|
-
|
31
29
|
rule(:dmarc_tag) do
|
30
|
+
dmarc_request |
|
32
31
|
dmarc_srequest |
|
33
32
|
dmarc_auri |
|
34
33
|
dmarc_furi |
|
@@ -48,6 +47,10 @@ module DMARC
|
|
48
47
|
end
|
49
48
|
end
|
50
49
|
|
50
|
+
tag_rule(:request,'p') do
|
51
|
+
str('none') | str('quarantine') | str('reject')
|
52
|
+
end
|
53
|
+
|
51
54
|
tag_rule(:srequest,'sp') do
|
52
55
|
str('none') | str('quarantine') | str('reject')
|
53
56
|
end
|
@@ -167,13 +170,22 @@ module DMARC
|
|
167
170
|
|
168
171
|
class Transform < Parslet::Transform
|
169
172
|
|
170
|
-
rule(:
|
173
|
+
rule(fo_opt: simple(:fo_opt)) { fo_opt }
|
174
|
+
rule(fo: simple(:fo_opt)) { {fo: [fo_opt]} }
|
175
|
+
|
176
|
+
rule(v: simple(:version)) { {v: version.to_sym} }
|
177
|
+
rule(p: simple(:p)) { {p: p.to_sym } }
|
178
|
+
rule(sp: simple(:sp)) { {sp: sp.to_sym} }
|
179
|
+
rule(rf: simple(:rf)) { {rf: rf.to_sym} }
|
180
|
+
rule(adkim: simple(:adkim)) { {adkim: adkim.to_sym} }
|
181
|
+
rule(aspf: simple(:aspf)) { {aspf: aspf.to_sym} }
|
171
182
|
|
172
|
-
rule(:
|
173
|
-
rule(:
|
183
|
+
rule(pct: simple(:pct)) { {pct: pct.to_i} }
|
184
|
+
rule(ri: simple(:ri)) { {ri: ri.to_i} }
|
174
185
|
|
175
|
-
rule(:
|
176
|
-
rule(:
|
186
|
+
rule(uri: simple(:uri)) { URI.parse(uri) }
|
187
|
+
rule(rua: subtree(:uris)) { {rua: Array(uris)} }
|
188
|
+
rule(ruf: subtree(:uris)) { {ruf: Array(uris)} }
|
177
189
|
|
178
190
|
end
|
179
191
|
|
@@ -186,8 +198,8 @@ module DMARC
|
|
186
198
|
# @return [Hash{Symbol => Object}]
|
187
199
|
# The Hash of tags within the record.
|
188
200
|
#
|
189
|
-
def parse(record)
|
190
|
-
tags = Transform.new.apply(
|
201
|
+
def self.parse(record)
|
202
|
+
tags = Transform.new.apply(new.parse(record))
|
191
203
|
hash = {}
|
192
204
|
|
193
205
|
tags.each { |tag| hash.merge!(tag) }
|
data/lib/dmarc/record.rb
CHANGED
@@ -1,30 +1,203 @@
|
|
1
|
+
require 'dmarc/dmarc'
|
1
2
|
require 'dmarc/parser'
|
2
3
|
require 'dmarc/exceptions'
|
3
4
|
|
5
|
+
require 'resolv'
|
6
|
+
|
4
7
|
module DMARC
|
5
|
-
class Record
|
8
|
+
class Record
|
6
9
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
10
|
+
# `p` field.
|
11
|
+
#
|
12
|
+
# @return [:none, :quarantine, :reject]
|
13
|
+
attr_reader :p
|
14
|
+
|
15
|
+
# `rua` field.
|
16
|
+
#
|
17
|
+
# @return [Array<URI::MailTo>]
|
18
|
+
attr_reader :rua
|
12
19
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
pct: 100,
|
18
|
-
rf: 'afrf',
|
19
|
-
ri: 86400,
|
20
|
-
}
|
20
|
+
# `rua` field.
|
21
|
+
#
|
22
|
+
# @return [Array<URI::MailTo>]
|
23
|
+
attr_reader :ruf
|
21
24
|
|
25
|
+
# `sp` field.
|
26
|
+
#
|
27
|
+
# @return [:none, :quarantine, :reject]
|
28
|
+
attr_reader :sp
|
29
|
+
|
30
|
+
# `v` field.
|
31
|
+
#
|
32
|
+
# @return [:DMARC1]
|
33
|
+
attr_reader :v
|
34
|
+
|
35
|
+
#
|
36
|
+
# Initializes the record.
|
37
|
+
#
|
38
|
+
# @param [Hash{Symbol => Object}] attributes
|
39
|
+
# Attributes for the record.
|
40
|
+
#
|
41
|
+
# @option attributes [:r, :s] :adkim (:r)
|
42
|
+
#
|
43
|
+
# @option attributes [:r, :s] :aspf (:r)
|
44
|
+
#
|
45
|
+
# @option attributes [Array<'0', '1', 'd', 's'>] :fo ('0')
|
46
|
+
#
|
47
|
+
# @option attributes [:none, :quarantine, :reject] :p
|
48
|
+
#
|
49
|
+
# @option attributes [Integer] :pct (100)
|
50
|
+
#
|
51
|
+
# @option attributes [:afrf, :iodef] :rf (:afrf)
|
52
|
+
#
|
53
|
+
# @option attributes [Integer] :ri (86400)
|
54
|
+
#
|
55
|
+
# @option attributes [Array<URI::MailTo>] :rua
|
56
|
+
#
|
57
|
+
# @option attributes [Array<URI::MailTo>] :ruf
|
58
|
+
#
|
59
|
+
# @option attributes [:none, :quarantine, :reject] :sp
|
60
|
+
#
|
61
|
+
# @option attributes [:DMARC1] :v
|
62
|
+
#
|
22
63
|
def initialize(attributes={})
|
23
|
-
attributes.
|
24
|
-
|
64
|
+
@adkim, @aspf, @fo, @p, @pct, @rf, @ri, @rua, @ruf, @sp, @v = attributes.values_at(:adkim, :aspf, :fo, :p, :pct, :rf, :ri, :rua, :ruf, :sp, :v)
|
65
|
+
end
|
66
|
+
|
67
|
+
def sp
|
68
|
+
@sp || @p
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# `adkim=` field.
|
73
|
+
#
|
74
|
+
# @return [:r, :s]
|
75
|
+
#
|
76
|
+
def adkim
|
77
|
+
@adkim || :r
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# `aspf` field.
|
82
|
+
#
|
83
|
+
# @return [:r, :s]
|
84
|
+
#
|
85
|
+
def aspf
|
86
|
+
@aspf || :r
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# `fo` field.
|
91
|
+
#
|
92
|
+
# @return [Array<'0', '1', 'd', 's'>]
|
93
|
+
#
|
94
|
+
def fo
|
95
|
+
@fo || %w[0]
|
96
|
+
end
|
97
|
+
|
98
|
+
#
|
99
|
+
# `pct` field.
|
100
|
+
#
|
101
|
+
# @return [Integer]
|
102
|
+
#
|
103
|
+
def pct
|
104
|
+
@pct || 100
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# `rf` field.
|
109
|
+
#
|
110
|
+
# @return [:afrf, :iodef]
|
111
|
+
#
|
112
|
+
def rf
|
113
|
+
@rf || :afrf
|
114
|
+
end
|
115
|
+
|
116
|
+
#
|
117
|
+
# `ri` field.
|
118
|
+
#
|
119
|
+
# @return [Integer]
|
120
|
+
#
|
121
|
+
def ri
|
122
|
+
@ri || 86400
|
123
|
+
end
|
124
|
+
|
125
|
+
#
|
126
|
+
# Parses a DMARC record.
|
127
|
+
#
|
128
|
+
# @param [String] record
|
129
|
+
# The raw DMARC record.
|
130
|
+
#
|
131
|
+
# @return [Record]
|
132
|
+
# The parsed DMARC record.
|
133
|
+
#
|
134
|
+
# @raise [InvalidRecord]
|
135
|
+
# The DMARC record could not be parsed.
|
136
|
+
#
|
137
|
+
# @since 0.3.0
|
138
|
+
#
|
139
|
+
# @api public
|
140
|
+
#
|
141
|
+
def self.parse(record)
|
142
|
+
new(Parser.parse(record))
|
143
|
+
rescue Parslet::ParseFailed => error
|
144
|
+
raise(InvalidRecord.new(error.message,error.cause))
|
145
|
+
end
|
146
|
+
|
147
|
+
#
|
148
|
+
# @deprecated use {parse} instead.
|
149
|
+
#
|
150
|
+
def self.from_txt(rec)
|
151
|
+
parse(rec)
|
152
|
+
end
|
153
|
+
|
154
|
+
#
|
155
|
+
# Queries and parses the DMARC record for a domain.
|
156
|
+
#
|
157
|
+
# @param [String] domain
|
158
|
+
# The domain to query DMARC for.
|
159
|
+
#
|
160
|
+
# @param [Resolv::DNS] resolver
|
161
|
+
# The resolver to use.
|
162
|
+
#
|
163
|
+
# @return [Record, nil]
|
164
|
+
# The parsed DMARC record. If no DMARC record was found, `nil` will be
|
165
|
+
# returned.
|
166
|
+
#
|
167
|
+
# @raise [InvalidRecord]
|
168
|
+
# The DMARC record could not be parsed.
|
169
|
+
#
|
170
|
+
# @since 0.3.0
|
171
|
+
#
|
172
|
+
# @api public
|
173
|
+
#
|
174
|
+
def self.query(domain,resolver=Resolv::DNS.new)
|
175
|
+
if (dmarc = DMARC.query(domain,resolver))
|
176
|
+
parse(dmarc)
|
25
177
|
end
|
178
|
+
end
|
179
|
+
|
180
|
+
#
|
181
|
+
# Converts the record back to a DMARC String.
|
182
|
+
#
|
183
|
+
# @return [String]
|
184
|
+
#
|
185
|
+
def to_s
|
186
|
+
tags = []
|
187
|
+
|
188
|
+
tags << "v=#{@v}" if @v
|
189
|
+
tags << "p=#{@p}" if @p
|
190
|
+
tags << "sp=#{@sp}" if @sp
|
191
|
+
tags << "rua=#{@rua.join(',')}" if @rua
|
192
|
+
tags << "ruf=#{@ruf.join(',')}" if @ruf
|
193
|
+
tags << "adkim=#{@adkim}" if @adkim
|
194
|
+
tags << "aspf=#{@aspf}" if @aspf
|
195
|
+
tags << "ri=#{@ri}" if @ri
|
196
|
+
tags << "fo=#{@fo.join(':')}" if @fo
|
197
|
+
tags << "rf=#{@rf}" if @rf
|
198
|
+
tags << "pct=#{@pct}" if @pct
|
26
199
|
|
27
|
-
|
200
|
+
return tags.join('; ')
|
28
201
|
end
|
29
202
|
|
30
203
|
end
|