dnsbase
Copyright(c) Viktor Dukhovni 2020-2026
(c) Peter Duchovni 2020
LicenseBSD-3-Clause
Maintainerietf-dane@dukhovni.org
Stabilityunstable
Safe HaskellNone
LanguageGHC2024

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:

  • \C for any byte C appends C as a single byte (the byte after the backslash is taken literally, with one exception: a trailing backslash is rejected with D8BadEscape).
  • \DDD for three ASCII decimal digits with DDD <= 255 appends 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

Domain data type

data Domain where Source #

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 Domain (presentation form .).

Instances

Instances details
Presentable Domain Source #

Conversion to presentation form via a bytestring Builder.

Instance details

Defined in Net.DNSBase.Internal.Domain

Show Domain Source #

Shows the presentation form string, adding double quotes and additional string escapes as needed. To get the raw string, use presentString.

Instance details

Defined in Net.DNSBase.Internal.Domain

Eq Domain Source # 
Instance details

Defined in Net.DNSBase.Internal.Domain

Methods

(==) :: Domain -> Domain -> Bool #

(/=) :: Domain -> Domain -> Bool #

Ord Domain Source # 
Instance details

Defined in Net.DNSBase.Internal.Domain

Lift Domain Source # 
Instance details

Defined in Net.DNSBase.Internal.Domain

Methods

lift :: Quote m => Domain -> m Exp #

liftTyped :: forall (m :: Type -> Type). Quote m => Domain -> Code m Domain #

data DnsTriple Source #

An RRSet is uniquely idenfified by a name, type, class triple.

Instances

Instances details
Presentable DnsTriple Source # 
Instance details

Defined in Net.DNSBase.Internal.Domain

Show DnsTriple Source # 
Instance details

Defined in Net.DNSBase.Internal.Domain

Eq DnsTriple Source # 
Instance details

Defined in Net.DNSBase.Internal.Domain

data Host 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

Instances details
Presentable Host Source #

Conversion to presentation form via a bytestring Builder.

Instance details

Defined in Net.DNSBase.Internal.Domain

Show Host Source #

Shows the presentation form string, adding double quotes and additional string escapes as needed. To get the raw string, use presentString.

Instance details

Defined in Net.DNSBase.Internal.Domain

Methods

showsPrec :: Int -> Host -> ShowS #

show :: Host -> String #

showList :: [Host] -> ShowS #

Eq Host Source #

Case-insensitive equality on the wire form.

Instance details

Defined in Net.DNSBase.Internal.Domain

Methods

(==) :: Host -> Host -> Bool #

(/=) :: Host -> Host -> Bool #

Ord Host Source #

Case-insensitive order on the wire form.

Instance details

Defined in Net.DNSBase.Internal.Domain

Methods

compare :: Host -> Host -> Ordering #

(<) :: Host -> Host -> Bool #

(<=) :: Host -> Host -> Bool #

(>) :: Host -> Host -> Bool #

(>=) :: Host -> Host -> Bool #

max :: Host -> Host -> Host #

min :: Host -> Host -> Host #

fromHost :: Host -> Domain Source #

Coerce a Host to a Domain.

toHost :: Domain -> Host Source #

Coerce a Domain to a Host.

data Mbox Source #

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

Instances details
Presentable Mbox Source #

Conversion to presentation form via a bytestring Builder.

Instance details

Defined in Net.DNSBase.Internal.Domain

Show Mbox Source #

Shows the presentation form string, adding double quotes and additional string escapes as needed. To get the raw string, use presentString.

Instance details

Defined in Net.DNSBase.Internal.Domain

Methods

showsPrec :: Int -> Mbox -> ShowS #

show :: Mbox -> String #

showList :: [Mbox] -> ShowS #

Eq Mbox Source # 
Instance details

Defined in Net.DNSBase.Internal.Domain

Methods

(==) :: Mbox -> Mbox -> Bool #

(/=) :: Mbox -> Mbox -> Bool #

Ord Mbox Source # 
Instance details

Defined in Net.DNSBase.Internal.Domain

Methods

compare :: Mbox -> Mbox -> Ordering #

(<) :: Mbox -> Mbox -> Bool #

(<=) :: Mbox -> Mbox -> Bool #

(>) :: Mbox -> Mbox -> Bool #

(>=) :: Mbox -> Mbox -> Bool #

max :: Mbox -> Mbox -> Mbox #

min :: Mbox -> Mbox -> Mbox #

fromMbox :: Mbox -> Domain Source #

Coerce an Mbox to a Domain.

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

dnLit Source #

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 ShortByteString; dnLit 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.

mbLit Source #

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: Text -> Either e ShortByteString. The user can pass the same parser they pass to dnLit (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..63 except 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 Text bytes verbatim into the wire form. A Text is 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 >= 128 are rejected.
  • \X (any other single character) emits X as 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 \, a truncated \DDD, a non-digit in \DDD, or \DDD with value greater than 255.

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

(String input only) Source contained a Char with codepoint above 0xFF; the 8-bit path cannot encode it.

D8Non7Bit

When parsing a mailbox presentation form, the first label contained a non-ASCII byte.

Instances

Instances details
Show Domain8Err Source # 
Instance details

Defined in Net.DNSBase.Domain

Eq Domain8Err Source # 
Instance details

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

Expand
>>> import qualified Data.ByteString.Char8 as BC
>>> dn = makeDomain8 $ BC.pack "www.corp.acme.example"
>>> dn
Right "www.corp.acme.example."
>>> toLabels <$> dn
Right ["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

Expand
>>> dn = makeDomain8Str "www.corp.acme.example"
>>> dn
Right "www.corp.acme.example."
>>> toLabels <$> dn
Right ["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

Expand
>>> import qualified Data.ByteString.Char8 as BC
>>> dn = makeMbox8 $ BC.pack "john.smith@acme.example"
>>> dn
Right "john\\.smith.acme.example."
>>> toLabels <$> dn
Right ["john.smith","acme","example"]
>>> toMbox <$> dn
Right "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

Expand
>>> dn = makeMbox8Str "john.smith@acme.example"
>>> dn
Right "john\\.smith.acme.example."
>>> toLabels <$> dn
Right ["john.smith","acme","example"]
>>> toMbox <$> dn
Right "john.smith@acme.example"

Canonicalisation to lower case

canonicalise :: Domain -> Domain Source #

Canonicalise a Domain to lower-case form.

Working with labels

appendDomain :: Domain -> Domain -> Maybe Domain Source #

Given two Domains attempt to construct a new new domain consisting of the labels of the first, followed by the labels of the second. Fails (returns Nothing) if the result would be too long.

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

isLDHName :: Domain -> Bool Source #

Does the given Domain name consist entirely of LDH labels?

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.

equalWireHost :: Domain -> Domain -> Bool Source #

Case-insensitive equality of domain names.

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.