Module: MailerToGo::SPF::Hostname
- Defined in:
- lib/mailertogo/spf/hostname.rb
Overview
Is this untrusted input a name we are willing to resolve?
Everything else in this gem takes a hostname it trusts. This is the gate for the case where it did not come from your own database — a box on a page labelled "domain", an API parameter, a CSV somebody uploaded. Asking a resolver to walk an SPF chain is recursive DNS performed on request, so what gets handed to it should be a plausible DNS name and nothing else: anything that could steer the lookup somewhere unintended — a scheme, a port, a path, a query string, an address literal — is stripped or refused here, before any resolver sees it.
It is forgiving about SHAPE, because people paste "https://www.example.com/pricing" or "billing@example.com" into a box labelled "domain" and are not wrong to expect that to work, and strict about the RESULT: a syntactically valid hostname, or nil.
Distinct from Record.normalize_name, and deliberately so. That one lowercases a name that came out of an SPF record and drops its root dot; it never rejects, because a name inside a record is already as trusted as the record. This one is a gate, and its job is to say no.
Constant Summary collapse
- MAX_LENGTH =
RFC 1035 §2.3.4 — 253 octets of presentation form, 63 per label.
253- LABEL =
A DNS label. Underscores are allowed because SPF names are real targets people check —
_spf.google.comand_spf.mailertogo.netare the two most likely things anyone types into such a box after their own domain. /\A[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?\z/- IPV4 =
An address literal is not a name to resolve SPF at.
/\A\d{1,3}(\.\d{1,3}){3}\z/- TLD =
A TLD is alphabetic, so "1.2" and "v=spf1" never reach a resolver.
/\A[a-z]{2,}\z/
Class Method Summary collapse
-
.parse(input) ⇒ Object
The hostname to resolve, or nil when the input is not one.
- .valid?(input) ⇒ Boolean
Class Method Details
.parse(input) ⇒ Object
The hostname to resolve, or nil when the input is not one.
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/mailertogo/spf/hostname.rb', line 43 def parse(input) host = input.to_s.strip.downcase return nil if host.empty? host = host.sub(%r{\A[a-z][a-z0-9+.-]*://}, "") # a pasted URL host = host.split("@").last.to_s # an email address host = host[%r{\A[^/?#]*}].to_s # path, query, fragment host = host.split(":").first.to_s # never honour a supplied port host = host.delete_prefix("[").delete_suffix("]") # an IPv6 literal in URL form host = host.chomp(".") # the root dot is optional return nil if host.empty? || host.length > MAX_LENGTH return nil if host.match?(IPV4) # No IDN here: resolvers speak ASCII, and guessing at an encoding for # somebody's DNS name is worse than telling them to paste the A-label. labels = host.split(".", -1) return nil if labels.size < 2 return nil unless labels.all? { |label| label.match?(LABEL) } return nil unless labels.last.match?(TLD) host end |
.valid?(input) ⇒ Boolean
67 68 69 |
# File 'lib/mailertogo/spf/hostname.rb', line 67 def valid?(input) !parse(input).nil? end |