| Copyright | (c) Viktor Dukhovni 2020-2026 (c) Peter Duchovni 2020 |
|---|---|
| License | BSD-3-Clause |
| Maintainer | ietf-dane@dukhovni.org |
| Stability | unstable |
| Safe Haskell | None |
| Language | GHC2024 |
Net.DNSBase.Domain
Description
The Domain data type represents the wire form of DNS domain
names or mailbox names. The internal representation is not
exposed, but is basically a ShortByteString containing a
sequence of length-prefixed A-labels, including a terminal empty
label. The labels must not be longer than 63 octets, and the
total length of the wire form must not exceed 255 bytes.
The distinction between domain names and mailbox names exists only
at the level of presentation form, and they are otherwise the
same. The standard presentation form of a Domain uses '.'
as a label separator, escaping any (rare) literal '.'
characters that happen to be part of the label content, and a
terminal dot is appended to the last label. As a matter of
convenience, this module introduces an ad hoc /mailbox
presentation form/ of a multi-label Domain, which uses '@'
as the separator between the first and second labels, and any
literal '.' characters in the first label are not escaped. In
the mailbox presentation form, no terminal '.' is appended to
the address.
As ShortByteString values, labels are composed of arbitrary
Word8 elements. The only constraint is that each label is at
most 63 bytes.
This module implements Template Haskell splices for literal
domain names in application source files. Literal strings are
validated and converted to wire form at compile-time. The
IDN-aware splice (RFC 5890+, Punycode encoding of U-labels) is the
canonical dnLit; the byte-level splice that accepts arbitrary
8-bit labels is available as dnLit8:
let d = $$(dnLit mkDomain "m\x00fc\&nchen.example.com") :: Domain -- IDN-aware let d = $$(dnLit8 "haskell.example.com") :: Domain -- byte-level let m = $$(mbLit8 "some.user@example.com") :: Domain
dnLit takes the presentation-form parser as its first argument:
the mkDomain used above comes from the companion idna2008
package, which is the usual choice when IDN labels are expected.
For names known to be 8-bit clean (typically just ASCII), the
dnLit8 and mbLit8 splices skip IDN processing entirely.
The runtime equivalents are makeDomain8 and makeMbox8 (and
the matching makeDomain8Str / makeMbox8Str for String
input). They accept the RFC 1035 master-file syntax described
below and return either a Domain or a Domain8Err describing
why the input was rejected.
Escape handling matches RFC 1035 master-file syntax:
\Cfor any byteCappendsCas a single byte (the byte after the backslash is taken literally, with one exception: a trailing backslash is rejected withD8BadEscape).\DDDfor three ASCII decimal digits withDDD <= 255appends the byte with that decimal value.
Validation:
- Each label is 1..63 bytes (empty non-final labels are rejected
as
D8EmptyLabel; a sole'.'or empty input both denote the root domain). - The wire form (all labels plus the terminator) is at most 255
bytes; an overflow is reported as
D8WireTooLong.
Synopsis
- data Domain where
- pattern RootDomain :: Domain
- data DnsTriple = DnsTriple {}
- data Host
- fromHost :: Host -> Domain
- toHost :: Domain -> Host
- data Mbox
- fromMbox :: Mbox -> Domain
- toMbox :: Domain -> Mbox
- dnLit :: forall e (m :: Type -> Type). (Show e, MonadFail m, Quote m) => (Text -> Either e ShortByteString) -> String -> Code m Domain
- mbLit :: forall e (m :: Type -> Type). (Show e, MonadFail m, Quote m) => (Text -> Either e ShortByteString) -> String -> Code m Domain
- dnLit8 :: forall (m :: Type -> Type). (Quote m, MonadFail m) => String -> Code m Domain
- mbLit8 :: forall (m :: Type -> Type). (Quote m, MonadFail m) => String -> Code m Domain
- wireToDomain :: ShortByteString -> Maybe Domain
- decodePresentationDomain :: (Text -> Either e ShortByteString) -> Text -> Either (Maybe e) Domain
- decodePresentationMbox :: (Text -> Either e ShortByteString) -> Text -> Either (Maybe e) Domain
- data Domain8Err
- makeDomain8 :: ByteString -> Either Domain8Err Domain
- makeDomain8Str :: String -> Either Domain8Err Domain
- makeMbox8 :: ByteString -> Either Domain8Err Domain
- makeMbox8Str :: String -> Either Domain8Err Domain
- canonicalise :: Domain -> Domain
- appendDomain :: Domain -> Domain -> Maybe Domain
- consDomain :: ShortByteString -> Domain -> Maybe Domain
- unconsDomain :: Domain -> Maybe (ShortByteString, Domain)
- labelCount :: Domain -> Word
- fromLabels :: [ShortByteString] -> Maybe Domain
- toLabels :: Domain -> [ShortByteString]
- revLabels :: Domain -> [ByteString]
- commonSuffix :: Domain -> Domain -> Domain
- mbWireForm :: Domain -> SizedBuilder
- shortBytes :: Domain -> ShortByteString
- wireBytes :: Domain -> ByteString
- isLDHName :: Domain -> Bool
- isLDHLabel :: ShortByteString -> Bool
- compareWireHost :: Domain -> Domain -> Ordering
- equalWireHost :: Domain -> Domain -> Bool
- canonicalNameOrder :: Domain -> Domain -> Ordering
- sortDomains :: [Domain] -> [Domain]
Domain data type
This type holds the wire form of fully-qualified DNS domain names encoded as A-labels.
The encoding of valid domain names to presentation form (the
Presentable instance) performs any required escaping of
special characters to ensure lossless round-trip encoding and
decoding of valid DNS names, and compatibility with the
standard zone file format. Valid names are not limited to the
letter-digit-hyphen (LDH) syntax of hostnames, all 8-bit
characters are allowed in DNS names, subject to the 63-byte
limit on wire form label length and 255-byte limit on the
wire form domain name (including the terminal empty label).
Equality and comparison are based on the wire-form and are
case-sensitive. The Host newtype implements case-insensitive
equality and comparison over the same wire-form bytes. The
toHost and fromHost functions implement coercions between
the two types.
Bundled Patterns
| pattern RootDomain :: Domain | The root |
Instances
| Presentable Domain Source # | Conversion to presentation form via a bytestring |
Defined in Net.DNSBase.Internal.Domain Methods present :: Domain -> Builder -> Builder Source # presentLazy :: Domain -> ByteString -> ByteString Source # | |
| Show Domain Source # | Shows the presentation form string, adding double quotes and additional
string escapes as needed. To get the raw string, use |
| Eq Domain Source # | |
| Ord Domain Source # | |
| Lift Domain Source # | |
An RRSet is uniquely idenfified by a name, type, class triple.
Constructors
| DnsTriple | |
Fields | |
Instances
| Presentable DnsTriple Source # | |
Defined in Net.DNSBase.Internal.Domain Methods present :: DnsTriple -> Builder -> Builder Source # presentLazy :: DnsTriple -> ByteString -> ByteString Source # | |
| Show DnsTriple Source # | |
| Eq DnsTriple Source # | |
Coercible to/from a domain, but its presentation form is canonical (lower
case) and has no terminating ., unless this is the root domain.
Equality and order are on the wire form, but are case-insensitive.
Instances
| Presentable Host Source # | Conversion to presentation form via a bytestring |
Defined in Net.DNSBase.Internal.Domain Methods present :: Host -> Builder -> Builder Source # presentLazy :: Host -> ByteString -> ByteString Source # | |
| Show Host Source # | Shows the presentation form string, adding double quotes and additional
string escapes as needed. To get the raw string, use |
| Eq Host Source # | Case-insensitive equality on the wire form. |
| Ord Host Source # | Case-insensitive order on the wire form. |
Coercible to/from a domain, but its presentation form uses the @ sign
as the separator after the first label, and does not escape literal .
characters within the first label. The second and subsequent labels are
canonicalised to lower-case. No terminating . is appended unless this
is the root domain.
Equality and order are on the wire form, but are case-insensitive.
Instances
| Presentable Mbox Source # | Conversion to presentation form via a bytestring |
Defined in Net.DNSBase.Internal.Domain Methods present :: Mbox -> Builder -> Builder Source # presentLazy :: Mbox -> ByteString -> ByteString Source # | |
| Show Mbox Source # | Shows the presentation form string, adding double quotes and additional
string escapes as needed. To get the raw string, use |
| Eq Mbox Source # | |
| Ord Mbox Source # | |
toMbox :: Domain -> Mbox Source #
Coerce a Domain to an Mbox. This changes the presentation form
to one in which the first label is separated from the rest by an '@'
character, and any dots in the first label remain unescaped. No period
is appended after the last label. The same form can be parsed by:
provided the first label (localpart) uses only 7-bit ASCII characters.
Mailboxes with non-ASCII localparts (EAI addresses) must be valid UTF-8
and can only be parsed by decodePresentationMbox or mbLit, but
the presentation form of Mbox does escapes all non-ASCII bytes in
\DDD decimal form, and would be rejected by all the above parsers.
A UTF-8 presentation form that respects EAI addresses is not yet
available, and would probably want a new EAIMbox data type.
Domain and mailbox name literals
Arguments
| :: forall e (m :: Type -> Type). (Show e, MonadFail m, Quote m) | |
| => (Text -> Either e ShortByteString) | Parser |
| -> String | Input literal (source code shape) |
| -> Code m Domain |
Template-Haskell typed splice for a compile-time Domain
literal. The caller supplies a parser of type
; Text -> Either e ShortByteStringdnLit packs the
source String literal as Text, runs the parser at compile
time, additionally checks the bytes via wireToDomain, and
embeds the resulting Domain as a constant. An invalid literal
(parser failure or wire-shape failure) becomes a compile-time
error.
The dnsbase library deliberately does not bundle a domain
parser; users compose a parser of their choice and pass it in.
The natural source of validating parsers is the idna2008
package, whose parsers already operate on Text.
Template-Haskell staging forbids referring to a same-module
top-level binding from inside the splice, so the parser must
either be defined in an imported module or bound by a let
inside the splice; for a single-call site the latter is the
more compact form:
import qualified Text.IDNA2008 as I
example :: Domain
example = $$(let parser = fmap I.wireBytesShort . I.mkDomain
in dnLit parser "www.example.org")mkDomain runs strict IDNA2008 with default label forms and
no mappings, returning just the validated idna2008 library's
Domain object. The I.wireBytesShort function extracts the
wire form bytes needed by dnLit.
For looser policies (mappings, emoji domain tolerance, etc.) use
parseDomainOpt with an explicit LabelFormSet and IDNAOpts,
and discard the LabelInfo half of its result.
Hoisting the parser into a separate module avoids retyping the composition at every literal:
-- in MyDomainParsers.hs strictParser :: Text -> Either I.IdnaError ShortByteString strictParser = fmap I.wireBytesShort . I.mkDomain -- in any module that imports MyDomainParsers example :: Domain example = $$(dnLit strictParser "www.example.org")
The source literal is converted to Text before the parser is
invoked; literals whose UTF-8 byte length exceeds 1024 are
rejected as invalid without consulting the parser. The emitted
splice is a constant Domain value (the wire-form
ShortByteString is materialised once from its compile-time
Addr# literal on first evaluation); the splice itself runs no
runtime IDNA code, and the caller's binary carries no
idna2008 dependency unless the user imports it themselves.
Arguments
| :: forall e (m :: Type -> Type). (Show e, MonadFail m, Quote m) | |
| => (Text -> Either e ShortByteString) | Parser |
| -> String | Input literal (source code shape) |
| -> Code m Domain |
Template-Haskell typed splice for a compile-time mailbox
literal. Packs the source String literal as Text
(rejecting inputs longer than 1024 bytes) and hands it to
decodePresentationMbox: the localpart is parsed locally with
DNS-style escapes, and the post-separator domain text is passed
to the caller-supplied parser. An invalid literal (localpart
failure or domain-parser failure or combined-length
failure) becomes a compile-time error.
The parser argument has the same shape as dnLit's:
. The user can pass
the same parser they pass to Text -> Either e ShortByteStringdnLit (typically a composition
with idna2008), and the mailbox literal inherits the same IDN
policy for the domain portion of the name. See dnLit for the
standard idioms.
dnLit8 :: forall (m :: Type -> Type). (Quote m, MonadFail m) => String -> Code m Domain Source #
Template-Haskell splice for literal Domain names that are
validated and converted from presentation form to wire form
at compile-time. Example:
domain :: Domain domain = $$(dnLit8 "example.org")
This is the byte-level path: it accepts any 8-bit master-file
text but performs no IDN processing. For IDN-aware literals (RFC
5890+, Punycode A-label encoding) use dnLit with a parser from
the companion idna2008 package.
mbLit8 :: forall (m :: Type -> Type). (Quote m, MonadFail m) => String -> Code m Domain Source #
Template-Haskell splice for literal mailbox names. Example:
mbox :: Domain mbox = $$(mbLit8 "hostmaster@example.org")
Byte-level all the way through: the localpart and the domain
portion are both parsed by makeMbox8Str, which treats the
input as opaque 8-bit master-file text. For EAI/UTF-8 localpart
semantics use mbLit with an idna2008-style Text parser.
Conversions
Validating import from wire form
wireToDomain :: ShortByteString -> Maybe Domain Source #
Validating import of a wire-form ShortByteString as a
Domain. Returns Just iff the bytes are a well-formed DNS
domain on the wire:
- total length in
1..255, - every label length byte in
1..63except the trailing zero-byte root label, - label boundaries align exactly with the buffer end -- i.e. the terminating empty label's NUL length-byte is the last byte, and there is no truncation or trailing garbage.
Suitable for receiving bytes the caller cannot prove well-formed (e.g. labels handed back by a foreign library or another package). Wire-form bytes that come straight from the decoder in Net.DNSBase.Decode.Domain are already validated and do not need to round-trip through this check.
From presentation form with pluggable parsers
decodePresentationDomain Source #
Arguments
| :: (Text -> Either e ShortByteString) | Parser |
| -> Text | Input to be parsed |
| -> Either (Maybe e) Domain |
Decode a domain in presentation form. The caller supplies the
parser; this entry point validates the parser's output as a
wire-form ShortByteString that wireToDomain accepts.
When the parser returns an error e, the return value is
Left (Just e). If a buggy parser produces an invalid wire
form, the return value is Left Nothing.
decodePresentationMbox Source #
Arguments
| :: (Text -> Either e ShortByteString) | Parser |
| -> Text | Input to be parsed |
| -> Either (Maybe e) Domain |
Parse a presentation-form mailbox into a Domain.
The input is split at the first unescaped '@' if any;
otherwise at the first unescaped '.'; otherwise the entire
input is the localpart and the resulting Domain has a single
non-root label. A separator present but followed by empty
domain text (e.g. "postmaster@" or "postmaster.") is
treated the same way as if the separator were absent: the
localpart is one label, the domain part is the root domain.
Following EAI semantics (RFC 6532), the localpart's wire bytes are either pure 7-bit ASCII or a well-formed UTF-8 sequence. The localpart decoder:
- Copies unescaped
Textbytes verbatim into the wire form. ATextis already valid UTF-8, so the bytes for one codepoint are 1, 2, 3 or 4 bytes long depending on the codepoint; no decoding or validation is needed. \DDD(three ASCII decimal digits,0..127) emits the single ASCII byte with that value. Values>= 128are rejected.\X(any other single character) emitsXas a single ASCII byte;X's codepoint must be< 0x80.
The rules above apply only to the localpart -- the first
label of the mailbox name. The post-separator Text (if any)
is handed verbatim to the caller-supplied domain parser, which
decodes any remaining labels.
The parser's output is validated to be a wire-form
ShortByteString that wireToDomain accepts. If length
limits permit, the decoded localpart is prepended to form the
combined Domain.
When the parser returns an error e, the return value is
Left (Just e). If a buggy parser produces an invalid wire
form, the return value is Left Nothing.
Decoders for 8-bit presentation forms
data Domain8Err Source #
Failure modes for the byte-level 8-bit presentation-form parser.
Intentionally coarse and position-free. Callers that need richer
diagnostics should use the idna2008 parser.
Constructors
| D8LabelTooLong | A label exceeds 63 bytes. |
| D8WireTooLong | The wire form exceeds 255 bytes. |
| D8BadEscape | A backslash escape is malformed: a trailing
|
| D8EmptyLabel | An empty interior label (consecutive dots,
a leading dot followed by more input, or an
empty mailbox localpart with a non-empty
remainder after the unescaped |
| D8Non8Bit | ( |
| D8Non7Bit | When parsing a mailbox presentation form, the first label contained a non-ASCII byte. |
Instances
| Show Domain8Err Source # | |
Defined in Net.DNSBase.Domain Methods showsPrec :: Int -> Domain8Err -> ShowS # show :: Domain8Err -> String # showList :: [Domain8Err] -> ShowS # | |
| Eq Domain8Err Source # | |
Defined in Net.DNSBase.Domain | |
makeDomain8 :: ByteString -> Either Domain8Err Domain Source #
Construct a Domain object directly from a presentation form
ByteString.
The bytes are not treated as UTF-8 content, and IDNA processing does not
apply. Backslash-escape encoding aside, each 8-bit byte in the input is
copied verbatim into the wire-form domain. For Unicode IDN domain support,
see the parsers in the idna2008 package.
Example
>>>import qualified Data.ByteString.Char8 as BC>>>dn = makeDomain8 $ BC.pack "www.corp.acme.example">>>dnRight "www.corp.acme.example.">>>toLabels <$> dnRight ["www","corp","acme","example"]
makeDomain8Str :: String -> Either Domain8Err Domain Source #
Same as makeDomain8, but the input is a String, and
an error (Left) is also returned if any of the input
string's characters are outside the 8-bit range.
Note that UTF-8 encoding of names in the Latin-1 alphabet might
still produce surprising results. For parsing IDN domain names,
see the idna2008 package.
Example
>>>dn = makeDomain8Str "www.corp.acme.example">>>dnRight "www.corp.acme.example.">>>toLabels <$> dnRight ["www","corp","acme","example"]
makeMbox8 :: ByteString -> Either Domain8Err Domain Source #
Construct a Domain object directly from a /presentation
form/ ByteString representing a mailbox. As a convenience to
users, the separator between the first and remaining labels is
optionally the first unescaped '@' character, in which
case prior '.' characters in the first label do not need to
be escaped.
The first label must not contain any non-ASCII bytes (above 127),
or an error (Left) is returned.
The toMbox function can be used to coerce the resulting
Domain to an Mbox object whose presentation form uses an
'@' between the first and remaining labels and does not
escape dots in the first label.
Example
>>>import qualified Data.ByteString.Char8 as BC>>>dn = makeMbox8 $ BC.pack "john.smith@acme.example">>>dnRight "john\\.smith.acme.example.">>>toLabels <$> dnRight ["john.smith","acme","example"]>>>toMbox <$> dnRight "john.smith@acme.example"
makeMbox8Str :: String -> Either Domain8Err Domain Source #
Same as makeMbox8, but the input is a 'String, and
an error ('Left why') is also returned if any of the input
string's characters are outside the 8-bit range.
The first label must not contain any non-ASCII bytes (above 127),
or an error (Left) is returned.
Note that UTF-8 encoding of domain names in the Latin-1 alphabet
might still produce surprising results. For parsing IDN domain
names, see the idna2008 package.
Example
>>>dn = makeMbox8Str "john.smith@acme.example">>>dnRight "john\\.smith.acme.example.">>>toLabels <$> dnRight ["john.smith","acme","example"]>>>toMbox <$> dnRight "john.smith@acme.example"
Canonicalisation to lower case
Working with labels
consDomain :: ShortByteString -> Domain -> Maybe Domain Source #
Attempt to prepend the given label to the given domain, provided the label length is 63 bytes or less, and the resulting domain is not too long.
unconsDomain :: Domain -> Maybe (ShortByteString, Domain) Source #
Given a Domain, return a tuple containing its first unescaped label as a
ShortByteString and the remainder of the Domain after removing the first
label. Returns Nothing for the root domain.
labelCount :: Domain -> Word Source #
Given a 'Domain/, return its label count. The root domain has zero labels.
>>>labelCount $$(dnLit8 "example.org")2
>>>toLabels $$(mbLit8 "first.last@example.org")3
fromLabels :: [ShortByteString] -> Maybe Domain Source #
Given a constituent list of raw unescaped labels, construct the
corresponding wire form domain name. No label may be empty or longer than
63 bytes, and the number of labels + the sum of label lengths must not
exceed 254. The return value is Nothing if the length constraints are
violated.
fromLabels (toLabels dn) == Just dn
toLabels :: Domain -> [ShortByteString] Source #
Given a 'Domain/, return its constituent list of raw unescaped labels, most-significant (TLD) label last.
>>>toLabels $$(dnLit8 "example.org")["example","org"]
>>>toLabels $$(mbLit8 "first.last@example.org")["first.last","example","org"]
revLabels :: Domain -> [ByteString] Source #
Given a Domain, return its constituent list of raw unescaped labels in reverse order, with the TLD first.
>>>revLabels $$(dnLit8 "example.org")["org","example"]
>>>revLabels $$(mbLit8 "first.last@example.org")["org","example","first.last"]
commonSuffix :: Domain -> Domain -> Domain Source #
Return the longest common suffix of two input domains.
Binary serialization functions
mbWireForm :: Domain -> SizedBuilder Source #
Encode a Domain name without name compression
shortBytes :: Domain -> ShortByteString Source #
The wire form of a domain name, including the zero-valued length byte of the terminal empty label.
wireBytes :: Domain -> ByteString Source #
Return the wire form of a Domain name as a ByteString
Predicates
isLDHLabel :: ShortByteString -> Bool Source #
Is the given ShortByteString a valid non-empty LDH label?
Sorting and comparison
compareWireHost :: Domain -> Domain -> Ordering Source #
Case-insensitive comparison of the wire forms of domains.
canonicalNameOrder :: Domain -> Domain -> Ordering Source #
Canonical name order:
https://datatracker.ietf.org/doc/html/rfc4034#section-6.1. For sorting
lists of more than a few elements, it may be best to perform a decorate,
sort, undecorate via sortDomains.
sortDomains :: [Domain] -> [Domain] Source #
Perform a decorate, sort, undecorate sort to return a list of domains in canonical order.