dmarcator 0.6.1 → 0.8.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/ChangeLog.md +17 -0
- data/README.md +91 -8
- data/lib/dmarcator/query.rb +138 -24
- data/lib/dmarcator/record.rb +24 -16
- data/lib/dmarcator/report.rb +3 -1
- data/lib/dmarcator/resolver/doh.rb +22 -3
- data/lib/dmarcator/resolver.rb +3 -1
- data/lib/dmarcator/uri.rb +9 -4
- data/lib/dmarcator/version.rb +1 -1
- data/lib/dmarcator/xml_helper.rb +0 -24
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f09058293dac6f0e31294f06182d69502ee06bc152aa3eb2ef06c07515f4363b
|
|
4
|
+
data.tar.gz: 6e3aee91c8c8d098c0f066a6b301ef9bc36f42018078769f15e65d695d35ba8a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dd47e82f78219d828832c95912a84581ad589efaec940bc25749a0a38b49692805f9afd8030e0f0bd01b2b611dd05a36fec474a8beb9866be340ba9a2e327f39
|
|
7
|
+
data.tar.gz: 973b12e73708b1540e912063db142ca91e07c5e29cca86e32481222e25334448b8efe754113bfeec6fae591c6411f8a82d796429c7ac36c26e4c22c8c3b714fa
|
data/ChangeLog.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
### 0.8.0 / 2026-10-29
|
|
2
|
+
|
|
3
|
+
* Add `DMARCator.txt`, which returns every TXT record at a name — unfiltered, in response order, with multi-chunk records joined. It accepts service labels such as `_dmarc` and `_domainkey` through a new relaxed `validate_txt_name`, and lets callers detect duplicate DMARC records that `dmarc` deliberately hides.
|
|
4
|
+
* Rename the lookup methods after what they return: `DMARCator.dmarc` (was `DMARCator.query`) and `DMARCator::Record.lookup` (was `DMARCator::Record.query`). The old names remain as deprecated aliases and emit no runtime warnings.
|
|
5
|
+
* Apply `DMARCATOR_DNS_TIMEOUTS` when the library builds a plaintext resolver, instead of on every lookup. An injected resolver now keeps its own configuration.
|
|
6
|
+
* Document the lookup APIs, bulk-scan guidance and deprecations in the README.
|
|
7
|
+
|
|
8
|
+
### 0.7.0 / 2026-10-29
|
|
9
|
+
|
|
10
|
+
* Plaintext DNS lookups now fail closed: a lookup failure raises `DMARCator::ResolutionError` instead of being reported as a domain without a DMARC record. Previously a timeout or resolver failure looked like no policy, which failed open. This is a behavior change and applies to every transport.
|
|
11
|
+
* DoH responses must use the `application/dns-message` content type (RFC 8484) and are capped at 65,535 bytes.
|
|
12
|
+
* `DMARCator::Uri` no longer forwards calls to private methods of the wrapped `URI::MailTo`.
|
|
13
|
+
* Remove the unused `XmlHelper#get_int`/`#get_timestamp` helpers and the deprecated `Record.from_txt`.
|
|
14
|
+
* Memoize a missing report version instead of recomputing it, and scope spec constants under `DMARCator`.
|
|
15
|
+
* Add `changelog_uri` and `documentation_uri` gem metadata, and document that domains must be passed as A-labels.
|
|
16
|
+
* Add a CI workflow that runs the specs on the minimum supported Ruby and the current release, loads the gem with warnings enabled, fails on YARD warnings, and builds the gem.
|
|
17
|
+
|
|
1
18
|
### 0.6.1 / 2026-10-29
|
|
2
19
|
|
|
3
20
|
* Remove a redundant `attr_reader :sp` that `Record#sp` already overrode, fixing a `method redefined; discarding old sp` warning emitted under `-w` (as CI enables).
|
data/README.md
CHANGED
|
@@ -89,10 +89,82 @@ record.sp
|
|
|
89
89
|
Query the DMARC record for a domain:
|
|
90
90
|
|
|
91
91
|
```ruby
|
|
92
|
-
record = DMARCator::Record.
|
|
92
|
+
record = DMARCator::Record.lookup("twitter.com")
|
|
93
93
|
# => #<DMARCator::Record ... @p=:reject, @rua=[...], ...>
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
Domains must be ASCII (A-labels): pass punycode such as
|
|
97
|
+
`xn--mnchen-3ya.de` rather than `münchen.de`. A lookup failure raises
|
|
98
|
+
`DMARCator::ResolutionError`; `nil` only means the domain genuinely has no
|
|
99
|
+
DMARC record.
|
|
100
|
+
|
|
101
|
+
## Lookup APIs
|
|
102
|
+
|
|
103
|
+
The lookup methods are named after what they return:
|
|
104
|
+
|
|
105
|
+
| Method | Returns | Cost |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `DMARCator.txt(name)` | `Array<String>` — every TXT record at `name` | one lookup |
|
|
108
|
+
| `DMARCator.dmarc(domain)` | `String` — the DMARC record text, or `nil` | one lookup |
|
|
109
|
+
| `DMARCator::Record.lookup(domain)` | `Record` — parsed | one lookup + parse (~0.4 ms) |
|
|
110
|
+
| `DMARCator::Record.parse(text)` | `Record` — parsed from a string | parse only |
|
|
111
|
+
|
|
112
|
+
`DMARCator.txt` is the primitive: unfiltered, in response order, multi-chunk
|
|
113
|
+
records joined. It works for any TXT name, including `_dmarc.` and
|
|
114
|
+
`_domainkey.` labels, and leaves interpretation to the caller:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
DMARCator.txt("_dmarc.example.com")
|
|
118
|
+
# => ["v=DMARC1; p=reject; rua=mailto:dmarc@example.com"]
|
|
119
|
+
|
|
120
|
+
DMARCator.txt("example.com")
|
|
121
|
+
# => ["v=spf1 include:_spf.example.com -all", "google-site-verification=abc"]
|
|
122
|
+
|
|
123
|
+
DMARCator.txt("s1._domainkey.example.com")
|
|
124
|
+
# => []
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Because it returns every record, it is also how you detect misconfiguration
|
|
128
|
+
that `DMARCator.dmarc` deliberately hides per RFC 7489 §6.6.3:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
records = DMARCator.txt("_dmarc.#{domain}").select { |t| t.start_with?("v=DMARC1") }
|
|
132
|
+
case records.length
|
|
133
|
+
when 0 then :none
|
|
134
|
+
when 1 then :ok
|
|
135
|
+
else :duplicate # more than one DMARC record breaks enforcement
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
All three methods accept an injected resolver, and an injected resolver keeps
|
|
140
|
+
its own configuration (timeouts are only applied to resolvers the library
|
|
141
|
+
builds):
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
resolver = DMARCator::Resolver::DoH.new(url: "https://dns.example/dns-query")
|
|
145
|
+
DMARCator.txt("_dmarc.example.com", resolver: resolver)
|
|
146
|
+
DMARCator.dmarc("example.com", resolver: resolver)
|
|
147
|
+
DMARCator::Record.lookup("example.com", resolver: resolver)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Bulk lookups
|
|
151
|
+
|
|
152
|
+
`DMARCator.dmarc` is the cheapest DMARC path — one lookup, no parsing. For a
|
|
153
|
+
scan over many domains:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
resolver = DMARCator.default_resolver # build once, not per domain
|
|
157
|
+
domains.each do |domain|
|
|
158
|
+
record = DMARCator.dmarc(domain, resolver: resolver)
|
|
159
|
+
# parse only when you need the fields
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Notes for large scans: `Record.lookup` adds a parse per domain (~0.4 ms, far
|
|
164
|
+
more than the lookup on a fast resolver); `Resolv::DNS` does not lock per query,
|
|
165
|
+
so use one resolver per thread when parallelising; and DoT/DoH open a new TLS
|
|
166
|
+
connection per lookup, so plaintext is the better choice for bulk work.
|
|
167
|
+
|
|
96
168
|
## Strict parsing
|
|
97
169
|
|
|
98
170
|
By default, unknown tags and tags with invalid values are ignored. Pass
|
|
@@ -149,15 +221,17 @@ hostname.
|
|
|
149
221
|
|
|
150
222
|
Encrypted transports fail closed: a transport failure raises
|
|
151
223
|
`DMARCator::ResolutionError` instead of being reported as a domain without a
|
|
152
|
-
DMARC record.
|
|
153
|
-
|
|
154
|
-
|
|
224
|
+
DMARC record. This applies to every transport, including plaintext —
|
|
225
|
+
`Resolv` returns an empty result for NXDOMAIN and raises for timeouts and
|
|
226
|
+
other failures, so a failed lookup is never mistaken for an absent record.
|
|
227
|
+
Plaintext remains the default because forcing a public resolver breaks private
|
|
228
|
+
and split-horizon zones, and some networks block port 853 or public resolvers.
|
|
155
229
|
|
|
156
230
|
A resolver can also be built explicitly:
|
|
157
231
|
|
|
158
232
|
```ruby
|
|
159
233
|
resolver = DMARCator::Resolver::DoH.new(url: "https://dns.example/dns-query")
|
|
160
|
-
record = DMARCator::Record.
|
|
234
|
+
record = DMARCator::Record.lookup("example.com", resolver: resolver)
|
|
161
235
|
```
|
|
162
236
|
|
|
163
237
|
## Security
|
|
@@ -181,7 +255,8 @@ DMARC records and XML reports are treated as untrusted input:
|
|
|
181
255
|
TLS certificates and fail closed: a transport failure raises
|
|
182
256
|
`DMARCator::ResolutionError` rather than being reported as a domain without a
|
|
183
257
|
DMARC record, so a blocked or downgraded lookup can never silently enforce no
|
|
184
|
-
policy.
|
|
258
|
+
policy. Plaintext lookups fail closed the same way. DoH responses must be
|
|
259
|
+
`application/dns-message` and are capped at 65,535 bytes.
|
|
185
260
|
|
|
186
261
|
## Requirements
|
|
187
262
|
|
|
@@ -208,8 +283,12 @@ was built, so a gem can always be traced back to its source:
|
|
|
208
283
|
# 2. Commit the release.
|
|
209
284
|
git commit -am "Bump version to X.Y.Z"
|
|
210
285
|
|
|
211
|
-
# 3. Tag that commit before building anything
|
|
212
|
-
|
|
286
|
+
# 3. Tag that commit before building anything, with release notes in the
|
|
287
|
+
# tag message. Repeating -m adds blank-line-separated paragraphs.
|
|
288
|
+
git tag -a vX.Y.Z \
|
|
289
|
+
-m "dmarcator X.Y.Z" \
|
|
290
|
+
-m "Summary of the release, including behaviour changes and upgrade notes." \
|
|
291
|
+
-m "See ChangeLog.md for the itemized list."
|
|
213
292
|
|
|
214
293
|
# 4. Push the commit and its tags.
|
|
215
294
|
git push origin main --follow-tags
|
|
@@ -219,6 +298,10 @@ gem build dmarc.gemspec
|
|
|
219
298
|
gem push dmarcator-X.Y.Z.gem
|
|
220
299
|
```
|
|
221
300
|
|
|
301
|
+
The tag message is the release note: it is what `git show vX.Y.Z` prints, so
|
|
302
|
+
put the highlight summary and any upgrade guidance there. Write it before step
|
|
303
|
+
4, because a pushed tag message cannot be changed without force-pushing the tag.
|
|
304
|
+
|
|
222
305
|
Tag before publishing, never after: a tag without a published gem, or a gem
|
|
223
306
|
without a tag, makes the two hard to reconcile later.
|
|
224
307
|
|
data/lib/dmarcator/query.rb
CHANGED
|
@@ -17,8 +17,18 @@ module DMARCator
|
|
|
17
17
|
#
|
|
18
18
|
# DNS query timeouts in seconds, tried in order.
|
|
19
19
|
#
|
|
20
|
+
# Applied when the library builds a plaintext resolver. Timeouts on an
|
|
21
|
+
# injected resolver are left alone.
|
|
22
|
+
#
|
|
20
23
|
DNS_TIMEOUTS = [2, 4, 8].freeze
|
|
21
24
|
|
|
25
|
+
#
|
|
26
|
+
# The maximum length of a textual DNS name, in octets.
|
|
27
|
+
#
|
|
28
|
+
# @since 0.8.0
|
|
29
|
+
#
|
|
30
|
+
MAX_TXT_NAME_LENGTH = 253
|
|
31
|
+
|
|
22
32
|
#
|
|
23
33
|
# Determines the version of the loaded `resolv` gem, if available.
|
|
24
34
|
#
|
|
@@ -57,6 +67,10 @@ module DMARCator
|
|
|
57
67
|
#
|
|
58
68
|
# Validates a domain before using it in a DNS query.
|
|
59
69
|
#
|
|
70
|
+
# Rejects underscores and reserves room for the `_dmarc.` prefix, so this is
|
|
71
|
+
# the validator for registrable domains rather than arbitrary DNS names.
|
|
72
|
+
# Use {validate_txt_name} for service labels.
|
|
73
|
+
#
|
|
60
74
|
# @param [String] domain
|
|
61
75
|
# The domain to validate.
|
|
62
76
|
#
|
|
@@ -79,6 +93,37 @@ module DMARCator
|
|
|
79
93
|
domain
|
|
80
94
|
end
|
|
81
95
|
|
|
96
|
+
#
|
|
97
|
+
# Validates an arbitrary DNS name used for a TXT lookup.
|
|
98
|
+
#
|
|
99
|
+
# Unlike {validate_domain}, service labels such as `_dmarc` and `_domainkey`
|
|
100
|
+
# are allowed, so names may contain underscores. Length and label limits are
|
|
101
|
+
# still enforced so hostile input cannot reach the resolver unbounded.
|
|
102
|
+
#
|
|
103
|
+
# @param [String] name
|
|
104
|
+
# The DNS name to validate.
|
|
105
|
+
#
|
|
106
|
+
# @return [String]
|
|
107
|
+
# The validated name.
|
|
108
|
+
#
|
|
109
|
+
# @raise [ArgumentError]
|
|
110
|
+
# The name is not a syntactically valid DNS name.
|
|
111
|
+
#
|
|
112
|
+
# @since 0.8.0
|
|
113
|
+
#
|
|
114
|
+
def self.validate_txt_name(name)
|
|
115
|
+
unless name.is_a?(String) && !name.empty? &&
|
|
116
|
+
name.length <= MAX_TXT_NAME_LENGTH &&
|
|
117
|
+
!name.start_with?('.') &&
|
|
118
|
+
!name.include?('..') &&
|
|
119
|
+
name.match?(/\A[A-Za-z0-9._-]+\z/) &&
|
|
120
|
+
name.split('.').all? { |label| label.length <= 63 && !label.start_with?('-') && !label.end_with?('-') }
|
|
121
|
+
raise ArgumentError, "invalid TXT name: #{name.inspect}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
name
|
|
125
|
+
end
|
|
126
|
+
|
|
82
127
|
#
|
|
83
128
|
# Builds the resolver configured by the environment.
|
|
84
129
|
#
|
|
@@ -94,47 +139,116 @@ module DMARCator
|
|
|
94
139
|
end
|
|
95
140
|
|
|
96
141
|
#
|
|
97
|
-
#
|
|
142
|
+
# Looks up every TXT record published at a DNS name.
|
|
143
|
+
#
|
|
144
|
+
# Records are returned exactly as published: unfiltered, in response order,
|
|
145
|
+
# and with multi-chunk records joined. Interpreting them (SPF, DKIM, DMARC,
|
|
146
|
+
# domain verification) is left to the caller. Use this when the individual
|
|
147
|
+
# records matter, for example to detect that a domain publishes more than
|
|
148
|
+
# one DMARC record.
|
|
149
|
+
#
|
|
150
|
+
# @param [String] name
|
|
151
|
+
# The DNS name to look up, such as `example.com`, `_dmarc.example.com`, or
|
|
152
|
+
# `s1._domainkey.example.com`.
|
|
153
|
+
#
|
|
154
|
+
# @param [Resolv::DNS, Resolver::DoH, Resolver::DoT] resolver
|
|
155
|
+
# The resolver to use. When omitted, one is built from `DMARCATOR_DNS`.
|
|
156
|
+
# An injected resolver keeps its own configuration.
|
|
157
|
+
#
|
|
158
|
+
# @return [Array<String>]
|
|
159
|
+
# The TXT records, or an empty array when the name has none. NXDOMAIN and
|
|
160
|
+
# NODATA are indistinguishable here.
|
|
161
|
+
#
|
|
162
|
+
# @raise [ResolutionError]
|
|
163
|
+
# The lookup failed. A failure is never reported as an empty result, so a
|
|
164
|
+
# blocked or degraded resolver cannot be mistaken for an absent record.
|
|
165
|
+
#
|
|
166
|
+
# @raise [ArgumentError]
|
|
167
|
+
# The name is not a syntactically valid DNS name.
|
|
168
|
+
#
|
|
169
|
+
# @note Names must be ASCII: pass punycode (A-labels) rather than Unicode.
|
|
170
|
+
#
|
|
171
|
+
# @since 0.8.0
|
|
172
|
+
#
|
|
173
|
+
def self.txt(name, resolver: default_resolver)
|
|
174
|
+
lookup_txt(validate_txt_name(name), resolver: resolver)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
#
|
|
178
|
+
# Looks up the DMARC record text published for a domain.
|
|
179
|
+
#
|
|
180
|
+
# This is the lowest-cost DMARC path: one TXT lookup and no parsing. Use it
|
|
181
|
+
# for bulk scans, and call {Record.parse} only on records you care about.
|
|
98
182
|
#
|
|
99
183
|
# @param [String] domain
|
|
100
|
-
# The domain to
|
|
184
|
+
# The domain to look up DMARC for.
|
|
101
185
|
#
|
|
102
186
|
# @param [Resolv::DNS, Resolver::DoH, Resolver::DoT] resolver
|
|
103
|
-
# The resolver to use. When omitted, one is built from `DMARCATOR_DNS
|
|
104
|
-
# plaintext resolvers receive {DNS_TIMEOUTS}.
|
|
187
|
+
# The resolver to use. When omitted, one is built from `DMARCATOR_DNS`.
|
|
105
188
|
#
|
|
106
189
|
# @return [String, nil]
|
|
107
|
-
# The
|
|
190
|
+
# The DMARC record text, or `nil` when the domain publishes none or
|
|
191
|
+
# publishes more than one (per RFC 7489 §6.6.3). Use {txt} to see every
|
|
192
|
+
# record, including duplicates.
|
|
108
193
|
#
|
|
109
|
-
# @raise [
|
|
110
|
-
#
|
|
111
|
-
# a transport problem is never reported as a missing record.
|
|
194
|
+
# @raise [ResolutionError]
|
|
195
|
+
# The lookup failed.
|
|
112
196
|
#
|
|
113
|
-
# @
|
|
197
|
+
# @raise [ArgumentError]
|
|
198
|
+
# The domain is not a syntactically valid DNS name.
|
|
114
199
|
#
|
|
115
|
-
# @since 0.
|
|
200
|
+
# @since 0.8.0
|
|
116
201
|
#
|
|
117
|
-
def self.
|
|
118
|
-
validate_domain(domain)
|
|
202
|
+
def self.dmarc(domain, resolver: default_resolver)
|
|
203
|
+
name = "_dmarc.#{validate_domain(domain)}"
|
|
119
204
|
|
|
120
|
-
|
|
205
|
+
# Per RFC 7489 §6.6.3, use the TXT record beginning with "v=DMARC1".
|
|
206
|
+
# If multiple DMARC records exist, treat the domain as having none.
|
|
207
|
+
dmarc_records = lookup_txt(name, resolver: resolver).select do |text|
|
|
208
|
+
text.start_with?('v=DMARC1')
|
|
209
|
+
end
|
|
121
210
|
|
|
122
|
-
|
|
211
|
+
dmarc_records.first if dmarc_records.one?
|
|
212
|
+
end
|
|
123
213
|
|
|
214
|
+
#
|
|
215
|
+
# @deprecated Use {dmarc}, which names what it returns. Kept for
|
|
216
|
+
# compatibility; it will be removed in a future release.
|
|
217
|
+
#
|
|
218
|
+
# @see dmarc
|
|
219
|
+
#
|
|
220
|
+
def self.query(domain,resolver=default_resolver)
|
|
221
|
+
dmarc(domain, resolver: resolver)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
#
|
|
225
|
+
# Looks up the TXT records at an already-validated name.
|
|
226
|
+
#
|
|
227
|
+
# Shared by {txt} and {dmarc} so both use one transport path.
|
|
228
|
+
#
|
|
229
|
+
# @param [String] name
|
|
230
|
+
#
|
|
231
|
+
# @param [Resolv::DNS, Resolver::DoH, Resolver::DoT] resolver
|
|
232
|
+
#
|
|
233
|
+
# @return [Array<String>]
|
|
234
|
+
#
|
|
235
|
+
# @raise [ResolutionError]
|
|
236
|
+
#
|
|
237
|
+
# @api private
|
|
238
|
+
#
|
|
239
|
+
def self.lookup_txt(name, resolver:)
|
|
240
|
+
# Resolv returns an empty result for NXDOMAIN and raises for timeouts and
|
|
241
|
+
# other failures, so any exception here is a transport failure, not an
|
|
242
|
+
# absent record.
|
|
124
243
|
begin
|
|
125
|
-
records = resolver.getresources(
|
|
126
|
-
rescue Resolv::ResolvError
|
|
127
|
-
|
|
244
|
+
records = resolver.getresources(name, Resolv::DNS::Resource::IN::TXT)
|
|
245
|
+
rescue Resolv::ResolvError => error
|
|
246
|
+
raise ResolutionError, "DNS TXT lookup failed for #{name}: #{error.message}"
|
|
128
247
|
end
|
|
129
248
|
|
|
130
|
-
|
|
131
|
-
# If multiple DMARC records exist, treat the domain as having none.
|
|
132
|
-
dmarc_records = records.map { |record| record.strings.join }
|
|
133
|
-
.select { |text| text.start_with?('v=DMARC1') }
|
|
134
|
-
|
|
135
|
-
return dmarc_records.first if dmarc_records.one?
|
|
136
|
-
nil
|
|
249
|
+
records.map { |record| record.strings.join }
|
|
137
250
|
end
|
|
251
|
+
private_class_method :lookup_txt
|
|
138
252
|
end
|
|
139
253
|
|
|
140
254
|
if DMARCator.vulnerable_resolv?
|
data/lib/dmarcator/record.rb
CHANGED
|
@@ -288,13 +288,6 @@ module DMARCator
|
|
|
288
288
|
raise InvalidRecord.new(truncate_error_message(error.message), error.cause)
|
|
289
289
|
end
|
|
290
290
|
|
|
291
|
-
#
|
|
292
|
-
# @deprecated use {parse} instead.
|
|
293
|
-
#
|
|
294
|
-
def self.from_txt(rec)
|
|
295
|
-
parse(rec)
|
|
296
|
-
end
|
|
297
|
-
|
|
298
291
|
# Maximum length of error messages echoed from parsing untrusted input.
|
|
299
292
|
MAX_ERROR_MESSAGE_LENGTH = 500
|
|
300
293
|
|
|
@@ -319,31 +312,46 @@ module DMARCator
|
|
|
319
312
|
end
|
|
320
313
|
|
|
321
314
|
#
|
|
322
|
-
#
|
|
315
|
+
# Looks up and parses the DMARC record for a domain.
|
|
316
|
+
#
|
|
317
|
+
# Prefer {DMARCator.dmarc} for bulk work: this method adds a parse
|
|
318
|
+
# (roughly 0.4 ms) on top of the lookup.
|
|
323
319
|
#
|
|
324
320
|
# @param [String] domain
|
|
325
|
-
# The domain to
|
|
321
|
+
# The domain to look up DMARC for.
|
|
326
322
|
#
|
|
327
|
-
# @param [Resolv::DNS] resolver
|
|
328
|
-
# The resolver to use.
|
|
323
|
+
# @param [Resolv::DNS, DMARCator::Resolver::DoH, DMARCator::Resolver::DoT] resolver
|
|
324
|
+
# The resolver to use. When omitted, one is built from `DMARCATOR_DNS`.
|
|
329
325
|
#
|
|
330
326
|
# @return [Record, nil]
|
|
331
|
-
# The parsed DMARC record
|
|
332
|
-
# returned.
|
|
327
|
+
# The parsed DMARC record, or `nil` when the domain publishes none.
|
|
333
328
|
#
|
|
334
329
|
# @raise [InvalidRecord]
|
|
335
330
|
# The DMARC record could not be parsed.
|
|
336
331
|
#
|
|
337
|
-
# @
|
|
332
|
+
# @raise [DMARCator::ResolutionError]
|
|
333
|
+
# The lookup failed.
|
|
334
|
+
#
|
|
335
|
+
# @since 0.8.0
|
|
338
336
|
#
|
|
339
337
|
# @api public
|
|
340
338
|
#
|
|
341
|
-
def self.
|
|
342
|
-
if (dmarc = DMARCator.
|
|
339
|
+
def self.lookup(domain, resolver: DMARCator.default_resolver)
|
|
340
|
+
if (dmarc = DMARCator.dmarc(domain, resolver: resolver))
|
|
343
341
|
parse(dmarc)
|
|
344
342
|
end
|
|
345
343
|
end
|
|
346
344
|
|
|
345
|
+
#
|
|
346
|
+
# @deprecated Use {lookup}, which names what it returns. Kept for
|
|
347
|
+
# compatibility; it will be removed in a future release.
|
|
348
|
+
#
|
|
349
|
+
# @see lookup
|
|
350
|
+
#
|
|
351
|
+
def self.query(domain,resolver=DMARCator.default_resolver)
|
|
352
|
+
lookup(domain, resolver: resolver)
|
|
353
|
+
end
|
|
354
|
+
|
|
347
355
|
#
|
|
348
356
|
# Converts the record to a Hash.
|
|
349
357
|
#
|
data/lib/dmarcator/report.rb
CHANGED
|
@@ -25,6 +25,12 @@ module DMARCator
|
|
|
25
25
|
# Default connect/read timeout in seconds.
|
|
26
26
|
DEFAULT_TIMEOUT = 5
|
|
27
27
|
|
|
28
|
+
# Maximum accepted response body in bytes (the DNS message limit).
|
|
29
|
+
MAX_RESPONSE_BYTES = 65_535
|
|
30
|
+
|
|
31
|
+
# Media type RFC 8484 requires for DNS messages.
|
|
32
|
+
CONTENT_TYPE = 'application/dns-message'
|
|
33
|
+
|
|
28
34
|
# @return [URI::HTTPS] the DoH endpoint.
|
|
29
35
|
attr_reader :url
|
|
30
36
|
|
|
@@ -92,8 +98,8 @@ module DMARCator
|
|
|
92
98
|
http.ipaddr = @ipaddr if @ipaddr
|
|
93
99
|
|
|
94
100
|
request = Net::HTTP::Post.new(@url.request_uri)
|
|
95
|
-
request['content-type'] =
|
|
96
|
-
request['accept'] =
|
|
101
|
+
request['content-type'] = CONTENT_TYPE
|
|
102
|
+
request['accept'] = CONTENT_TYPE
|
|
97
103
|
request.body = query.encode
|
|
98
104
|
|
|
99
105
|
response = http.start { |connection| connection.request(request) }
|
|
@@ -104,7 +110,20 @@ module DMARCator
|
|
|
104
110
|
raise ResolutionError, "DoH endpoint returned HTTP #{status}"
|
|
105
111
|
end
|
|
106
112
|
|
|
107
|
-
response.
|
|
113
|
+
content_type = response['content-type'].to_s
|
|
114
|
+
|
|
115
|
+
unless content_type.start_with?(CONTENT_TYPE)
|
|
116
|
+
raise ResolutionError, "DoH endpoint returned unexpected content-type: #{content_type.inspect}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
body = response.body.to_s.b
|
|
120
|
+
|
|
121
|
+
if body.bytesize > MAX_RESPONSE_BYTES
|
|
122
|
+
raise ResolutionError,
|
|
123
|
+
"DoH response exceeds maximum size of #{MAX_RESPONSE_BYTES} bytes"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
body
|
|
108
127
|
rescue ResolutionError
|
|
109
128
|
raise
|
|
110
129
|
rescue StandardError => error
|
data/lib/dmarcator/resolver.rb
CHANGED
data/lib/dmarcator/uri.rb
CHANGED
|
@@ -118,19 +118,24 @@ module DMARCator
|
|
|
118
118
|
private
|
|
119
119
|
|
|
120
120
|
#
|
|
121
|
-
# Pass
|
|
121
|
+
# Pass public methods through to {#uri}.
|
|
122
|
+
#
|
|
123
|
+
# Private methods are not forwarded: delegation is limited to the wrapped
|
|
124
|
+
# URI's public interface.
|
|
122
125
|
#
|
|
123
126
|
def method_missing(name,*arguments,&block)
|
|
124
|
-
@uri.
|
|
127
|
+
return super unless @uri.respond_to?(name)
|
|
128
|
+
|
|
129
|
+
@uri.public_send(name,*arguments,&block)
|
|
125
130
|
end
|
|
126
131
|
|
|
127
132
|
#
|
|
128
|
-
# Indicates whether a missing method can be handled by {#uri}.
|
|
133
|
+
# Indicates whether a missing public method can be handled by {#uri}.
|
|
129
134
|
#
|
|
130
135
|
# @return [Boolean]
|
|
131
136
|
#
|
|
132
137
|
def respond_to_missing?(name, include_private = false)
|
|
133
|
-
@uri.respond_to?(name
|
|
138
|
+
@uri.respond_to?(name) || super
|
|
134
139
|
end
|
|
135
140
|
|
|
136
141
|
end
|
data/lib/dmarcator/version.rb
CHANGED
data/lib/dmarcator/xml_helper.rb
CHANGED
|
@@ -20,30 +20,6 @@ module DMARCator
|
|
|
20
20
|
@node.at_xpath(xpath)&.text
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
#
|
|
24
|
-
# Returns a Unix timestamp as a `Time`.
|
|
25
|
-
#
|
|
26
|
-
# @param [String] xpath
|
|
27
|
-
# The XPath expression to match.
|
|
28
|
-
#
|
|
29
|
-
# @return [Time, nil]
|
|
30
|
-
#
|
|
31
|
-
def get_timestamp(xpath)
|
|
32
|
-
parse_timestamp(get_text(xpath))
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
#
|
|
36
|
-
# Returns an integer value.
|
|
37
|
-
#
|
|
38
|
-
# @param [String] xpath
|
|
39
|
-
# The XPath expression to match.
|
|
40
|
-
#
|
|
41
|
-
# @return [Integer, nil]
|
|
42
|
-
#
|
|
43
|
-
def get_int(xpath)
|
|
44
|
-
parse_integer(get_text(xpath))
|
|
45
|
-
end
|
|
46
|
-
|
|
47
23
|
#
|
|
48
24
|
# Parses a Unix timestamp from raw element text.
|
|
49
25
|
#
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dmarcator
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Noah Berman
|
|
@@ -92,6 +92,8 @@ licenses:
|
|
|
92
92
|
metadata:
|
|
93
93
|
source_code_uri: https://github.com/bermannoah/dmarcator
|
|
94
94
|
bug_tracker_uri: https://github.com/bermannoah/dmarcator/issues
|
|
95
|
+
changelog_uri: https://github.com/bermannoah/dmarcator/blob/main/ChangeLog.md
|
|
96
|
+
documentation_uri: https://rubydoc.info/gems/dmarcator
|
|
95
97
|
rubygems_mfa_required: 'true'
|
|
96
98
|
rdoc_options: []
|
|
97
99
|
require_paths:
|