1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
|
-- | Miscellaneous utility functions for processing DNS data.
--
module Network.DNS.Utils (
normalize
, normalizeCase
, normalizeRoot
, splitDomain
, splitMailbox
) where
import qualified Data.ByteString.Char8 as BS
import Data.Char (toLower)
import Network.DNS.Types.Internal (DNSError, Domain, Mailbox)
import Network.DNS.StateBinary (parseLabel)
-- | Perform both 'normalizeCase' and 'normalizeRoot' on the given
-- 'Domain'. When comparing DNS names taken from user input, this is
-- often necessary to avoid unexpected results.
--
-- /Examples/:
--
-- >>> let domain1 = BS.pack "ExAmPlE.COM"
-- >>> let domain2 = BS.pack "example.com."
-- >>> domain1 == domain2
-- False
-- >>> normalize domain1 == normalize domain2
-- True
--
-- The 'normalize' function should be idempotent:
--
-- >>> normalize (normalize domain1) == normalize domain1
-- True
--
-- Ensure that we don't crash on the empty 'Domain':
--
-- >>> import qualified Data.ByteString.Char8 as BS
-- >>> normalize BS.empty
-- "."
--
normalize :: Domain -> Domain
normalize = normalizeCase . normalizeRoot
-- | Normalize the case of the given DNS name for comparisons.
--
-- According to RFC #1035, \"For all parts of the DNS that are part
-- of the official protocol, all comparisons between character
-- strings (e.g., labels, domain names, etc.) are done in a
-- case-insensitive manner.\" This function chooses to lowercase
-- its argument, but that should be treated as an implementation
-- detail if at all possible.
--
-- /Examples/:
--
-- >>> let domain1 = BS.pack "ExAmPlE.COM"
-- >>> let domain2 = BS.pack "exAMPle.com"
-- >>> domain1 == domain2
-- False
-- >>> normalizeCase domain1 == normalizeCase domain2
-- True
--
-- The 'normalizeCase' function should be idempotent:
--
-- >>> normalizeCase (normalizeCase domain2) == normalizeCase domain2
-- True
--
-- Ensure that we don't crash on the empty 'Domain':
--
-- >>> import qualified Data.ByteString.Char8 as BS
-- >>> normalizeCase BS.empty
-- ""
--
normalizeCase :: Domain -> Domain
normalizeCase = BS.map toLower
-- | Normalize the given name by appending a trailing dot (the DNS
-- root) if one does not already exist.
--
-- Warning: this does not produce an equivalent DNS name! However,
-- users are often unaware of the effect that the absence of the
-- root will have. In user interface design, it may therefore be
-- wise to act as if the user supplied the trailing dot during
-- comparisons.
--
-- Per RFC #1034,
--
-- \"Since a complete domain name ends with the root label, this leads
-- to a printed form which ends in a dot. We use this property to
-- distinguish between:
--
-- * a character string which represents a complete domain name
-- (often called \'absolute\'). For example, \'poneria.ISI.EDU.\'
--
-- * a character string that represents the starting labels of a
-- domain name which is incomplete, and should be completed by
-- local software using knowledge of the local domain (often
-- called \'relative\'). For example, \'poneria\' used in the
-- ISI.EDU domain.
--
-- Relative names are either taken relative to a well known origin,
-- or to a list of domains used as a search list. Relative names
-- appear mostly at the user interface, where their interpretation
-- varies from implementation to implementation, and in master
-- files, where they are relative to a single origin domain name.\"
--
-- /Examples/:
--
-- >>> let domain1 = BS.pack "example.com"
-- >>> let domain2 = BS.pack "example.com."
-- >>> domain1 == domain2
-- False
-- >>> normalizeRoot domain1 == normalizeRoot domain2
-- True
--
-- The 'normalizeRoot' function should be idempotent:
--
-- >>> normalizeRoot (normalizeRoot domain1) == normalizeRoot domain1
-- True
--
-- Ensure that we don't crash on the empty 'Domain':
--
-- >>> import qualified Data.ByteString.Char8 as BS
-- >>> normalizeRoot BS.empty
-- "."
--
normalizeRoot :: Domain -> Domain
normalizeRoot d
| BS.null d = trailing_dot
| BS.last d == '.' = d
| otherwise = d `BS.append` trailing_dot
where
trailing_dot = BS.pack "."
-- | Split a domain name in A-label form into its initial label and the rest of
-- the domain. Returns an error if the initial label is malformed. When no
-- more labels remain, the initial label will satisfy 'BS.null'.
--
-- This also decodes any escaped characters in the initial label, which may
-- therefore contain whitespace, binary data, or unescaped internal dots. To
-- reconstruct the original domain, the initial label may sometimes require
-- correct escaping of special characters.
--
-- ==== __Examples__
--
-- >>> import Data.ByteString.Char8 as BS
-- >>> splitDomain $ BS.pack "abc\\.def.xyz"
-- Right ("abc.def","xyz")
--
-- >>> splitDomain $ BS.pack ".abc.def.xyz"
-- Left (DecodeError "invalid domain: .abc.def.xyz")
--
splitDomain :: Domain -> Either DNSError (BS.ByteString, Domain)
splitDomain = parseLabel 0x2e
-- | Split a 'Mailbox' in A-label form into its initial label 'BS.ByteString'
-- (the /localpart/ of the email address) and the remaining 'Domain' (the
-- /domainpart/ of the email address, with a possible trailing @'.'@). Returns
-- an error if the initial label is malformed. When no more labels remain, the
-- initial label will satisfy 'BS.null'. The remaining labels can be obtained
-- by applying 'splitDomain' the returned domain part.
--
-- This also decodes any escaped characters in the initial label, which may
-- therefore contain whitespace, binary data, or unescaped internal dots. To
-- reconstruct the original mailbox, the initial label may sometimes require
-- correct escaping of special characters.
--
-- ==== __Example__
--
-- >>> import Data.ByteString.Char8 as BS
-- >>> splitMailbox $ BS.pack "Joe.Admin@example.com."
-- Right ("Joe.Admin","example.com.")
--
splitMailbox :: Mailbox -> Either DNSError (BS.ByteString, Domain)
splitMailbox = parseLabel 0x40
|