dns 0.3.8 → 4.2.0
raw patch · 33 files changed
Files
- Changelog.md +279/−0
- Network/DNS.hs +45/−24
- Network/DNS/Decode.hs +90/−172
- Network/DNS/Encode.hs +28/−161
- Network/DNS/IO.hs +201/−0
- Network/DNS/Internal.hs +0/−208
- Network/DNS/Lookup.hs +396/−80
- Network/DNS/LookupRaw.hs +298/−0
- Network/DNS/Resolver.hs +87/−174
- Network/DNS/StateBinary.hs +0/−169
- Network/DNS/Transport.hs +245/−0
- Network/DNS/Types.hs +126/−18
- Network/DNS/Utils.hs +177/−0
- Setup.hs +2/−0
- cabal.project +3/−0
- cbits/dns.c +77/−0
- dns.cabal +134/−87
- internal/Network/DNS/Base32Hex.hs +50/−0
- internal/Network/DNS/Decode/Internal.hs +64/−0
- internal/Network/DNS/Decode/Parsers.hs +508/−0
- internal/Network/DNS/Encode/Builders.hs +361/−0
- internal/Network/DNS/Encode/Internal.hs +38/−0
- internal/Network/DNS/Imports.hs +31/−0
- internal/Network/DNS/Memo.hs +102/−0
- internal/Network/DNS/Resolver/Internal.hs +52/−0
- internal/Network/DNS/StateBinary.hs +441/−0
- internal/Network/DNS/Types/Internal.hs +1407/−0
- internal/Network/DNS/Types/Resolver.hs +136/−0
- test/DecodeSpec.hs +131/−0
- test/EncodeSpec.hs +74/−119
- test/RoundTripSpec.hs +267/−0
- test2/IOSpec.hs +38/−0
- test2/LookupSpec.hs +45/−53
+ Changelog.md view
@@ -0,0 +1,279 @@+# ChangeLog++## 4.2.0++- Switching cryptonite to crypton.++## 4.1.1++- Adding encoder/decoder for CAA RR.++## 4.1.0++- Breaking change: GHC 7.x and earlier no longer supported.+ We now require support for PatternSynonyms, available since+ GHC 8.0.+- Feature: relaxed lookup-raw interface+ [#167](https://github.com/kazu-yamamoto/dns/pull/167)+- Using "53" instead of "domain".+ [#166](ttps://github.com/kazu-yamamoto/dns/pull/166)+- UDP ReceiveFrom, sendTo with SockAddr+ [#165](https://github.com/kazu-yamamoto/dns/pull/165)+- Feature: Support for RP resource record type+ [#161](https://github.com/kazu-yamamoto/dns/pull/161)+- Feature: New `splitDomain` function splits a domain name+ at the first label break, unescaping the first label to+ a raw ByteString.+- Feature: New `splitMailbox` function splits a domain name+ at the first label break, unescaping the first label to+ a raw ByteString.+ [#155](https://github.com/kazu-yamamoto/dns/pull/155)+- Bugfix: Encoding of large packets could produce invalid+ compression pointers.+ [#156](https://github.com/kazu-yamamoto/dns/pull/156)+- Bugfix: SRV record presentation form (RD_SRV show instance)+ was missing a space between the port and the target.++## 4.0.1++- Bugfix: Retry without EDNS on empty FormatErr responses. Non-EDNS resolvers+ may return a FormErr response with an empty question section. Such a response+ must be accepted as a valid signal to switch to non-EDNS queries, even though+ the response does not contain a matching question.+- Feature: New RData constructors RD_CDS and RD_CDNSKEY+- Usability: More friendly network errors, instead of reporting the error+ location as an overly verbose "addrinfo" it is now just the essential+ "tcp@address" or "udp@address".+- BCP: The EDNS UDP buffer size has been changed to the RIPE recommended+ default of 1232 bytes. Note that this recomendation is for a default value,+ to be used when better information is not available. Users can still+ configure larger values if their networks support larger data frames and they+ are certain there is no risk of IP fragmentation.+- CI: Linux tests now pass with GHC 8.0.2, 8.2.2, 8.4.4, 8.6.5 and 8.8.1.+ Windows tests now build and run, but pass only intermittently. The Windows+ doctests hang most of the time, perhaps a bug or portability issue in the+ doctest code, rather than the DNS library?+- Build: Internal modules are no longer exposed outside the build, this uses+ Cabal 2.0 or later features to expose internal modules only to the test+ executables.++## 4.0.0++- Breaking change: when `Domain` name ByteStrings are+ parsed as a sequence of DNS labels, backslashed escapes+ (single-character and 3-digit decimal) are decoded to+ the corresponding character or byte. Therefore, `encode`+ is not a total function, it may raise a `DecodeError`+ when a `ResourceRecord` contains a malformed `Domain`.+- Breaking change: when wire-form DNS names are converted+ to `Domain` ByteStrings, special characters in DNS labels+ are now encoded as `\c` (single-character backslash escapes)+ and non-printing characters as `\DDD` (3-digit decimal escapes).+- Output format change: `show` for TXT RDATA now includes+ enclosing double quotes, and escapes special characters.+ This is consistent with the format of TXT records in zone+ files and, e.g., dig(1) output. The DNS string quoting+ syntax is similar to a proper subset of the Haskell string+ quoting syntax, but its decimal escapes require exactly+ three digits, while Haskell accepts 1 or more, and uses+ `'\&'` as a null. Therefore, `read @String` does not+ reliably decode the DNS text string presentation form.+- Breaking change: the DNSMessage __component__ encoding+ functions are now internal. They're still exported from+ the new 'Nework.DNS.Encode.Internal' module, but this+ is only to make them available for the test-suite.+- Added the TYPE definition, but not yet RData, for CAA.+- Added decode, encode and show for NSEC3 RRs.+- Added base16-bytestring as a new dependency.+- Added decode, encode and show for NSEC RRs.+- New RData constructor RD\_NSEC.+- Correct presentation form of unknown RR types.+- Corrected encoding of long TXT records+- RD\_NULL now has an opaque data payload.+- Safety: Both 'decode' and 'decodeAt' must now consume exactly+ the complete input buffer or a DecodeError is returned.+ * The same applies to each complete message with 'decodeMany'+ and 'decodeManyAt'. Any final encoded message segment at the+ end of the input buffer is still returned as the second+ element of the result pair.+- Bugfix: fixed incorrect decoding of TXT records, and corrected+ the associated test.+- Cleanup: More precise control over decoder error messages via+ 'failSGet', which avoids the unhelpful Attoparsec "Failure+ reading: " error prefix.+- Cleanup: Simplified loop detection in name decompression, making+ use of a monotone strictly decreasing limit on valid "pointer"+ targets.+- Breaking change: In the "Decode" module, expose only the+ decode{,Many}{,At} functions. The rest of the "Decode" module's+ functions are now internal, exposed only for testing. These+ include:+ * decodeDNSHeader+ * decodeDNSFlags+ * decodeResourceRecord+ * decodeDomain+ * decodeMailbox+- Cleanup: Reworked Decode module structure:+ * Moved Decode.Internal to Decode.Parsers+ * Created a new Decode.Internal which is now exposed, and+ moved some functions there from Decode which are only+ exposed for testing, since they could not reliably be used+ except as part of decoding a full message.+- Feature: RRSIG support, we can now encode, decode and show RRSIG+ records. This uses the new 'decodeAt' and 'decodeManyAt' API.+- New API: 'decodeAt' and 'decodeManyAt' make it possible for+ the decoder to get the current time, in order to decode some+ RR types (like RRSIG) whose full meaning is time-dependent.+- Re-export 'sendAll' and export 'encodeVC' for use with TCP.+- No longer using sendAll with UDP, UDP datagrams must not be sent+ piece-by-piece+- Removed socket I/O work-around for no longer supported GHC versions+ on Windows.+- TCP queries now also use EDNS, since the DO bit and other options+ may be relevant, even when the UDP buffer size is not. Therefore,+ TCP now also does a non-EDNS fallback.+- The resolvEDNS field is subsumed in resolvQueryControls and+ removed. The encodeQuestion function changes to no longer take+ an explicit "EDNSheader" argument, instead the EDNS record is+ built based on the supplied options. Also the encodeQuestions+ function has been removed, since we're deprecating it, but the+ legacy interface can no longer be maintained.+- New API: lookupRawCtl+- New API: ODataOp, doFlag, ednsEnable, ednsSetVersion, ednsSetSize+ and ednsSetOptions make it possible for 'QueryControls' to adjust+ EDNS settings.+- New API: FlagOp, rdFlag, adFlag and cdFlag make it possible to+ override the default settings of the query-related DNS header+ flags.+- Breaking change: the decoded EDNS record no longer contains+ an error field. Instead the header of decoded messages is+ updated hold the extended error code when valid EDNS OPT records+ (EDNS pseudo-headers) are found. The remaining EDNS record+ fields have been renamed:+ * udpSize -> ednsBufferSize+ * dnssecOk -> ednsDnssecOk+ * options -> ednsOptions+ The reverse process happens on output with the 12-bit header+ RCODE split across the wire-form DNS header and the OPT record.+ When EDNS is not enabled, and the RCODE > 15, it is mapped to+ FormatErr instead.+- Breaking change: The fromRCODEforHeader and toRCODEforHeader+ functions have been removed.+- Breaking change: DNSFormat and fromDNSFormat+ have been removed.+- The fromDNSMessage function now distinguishes between FormatErr+ responses without an OPT record (which signal no EDNS support),+ and FormatErr with an OPT record, which signal problems+ (malformed or unsupported version) with the OPT record received+ in the request. For the latter the 'BadOptRecord' error is+ returned.+- Added more RCODEs, including a BadRCODE that is generated+ locally, rather than parsed from the message. The value+ lies just above the EDNS 12-bit range, with the bottom+ 12-bits matching FormatErr.+- Breaking change: The DNSMessage structure now has an+ "ednsHeader" field, initialized to "EDNSheader defaultEDNS"+ in "defaultQuery" and to "NoEDNS" in "defaultResponse".+ The former enables EDNS(0) with default options, the latter+ leaves EDNS unconfigured.+- The BadOpt RCODE is renamed to BadVers to better resemble+ the term used in RFCs.+- Added EDNS OPTIONS: NSID, DAU, DHU, N3U+- Decoding of the ClientSubnet option is now a total function,+ provided the RDATA is structurally sound. Unexpected values+ just yield OD\_ECSgeneric results.+- Breaking change: New OD\_ECSgeneric EDNS constructor, represents+ ClientSubnet values whose address family is not IP or that violate+ the specification. The "family" field distinguishes the two cases.+- The ClientSubnet EDNS option is now encoded correctly even when the+ source bits match some trailing all-zero bytes.+- Breaking change: EDNS0 is renamed to EDNS.+- Breaking change: lookupRawAD, composeQuery, composeQueryAD are removed.+- New OP codes: OP\_NOTIFY and OP\_UPDATE.+ [#113](https://github.com/kazu-yamamoto/dns/pull/113)++## 3.0.4++- Drop unexpected UDP answers [#112](https://github.com/kazu-yamamoto/dns/pull/112)++## 3.0.3++- Implementing NSEC3PARAM [#109](https://github.com/kazu-yamamoto/dns/pull/109)+- Fixing an example of DNS server.+- Improving DNS decoder [#111](https://github.com/kazu-yamamoto/dns/pull/111)++## 3.0.2++- Supporting conduit 1.3 [#105](https://github.com/kazu-yamamoto/dns/pull/105)+- Supporting GHC 8.4 with semigroup hack.++## 3.0.1++- Supporting GHC 7.8 again.++## 3.0.0++- The version introduces some breaking changes internally. But upper layer APIs in the `Lookup` module remain the same.+- Breaking change: `Network.DNS.Types` is redesigned. `ResourceRecord` is not a sum type anymore. It holds only normal RRs. For EDNS0, a new scheme is implemented. [#63](https://github.com/kazu-yamamoto/dns/issues/63)+- Breaking change: the `Encode` and `Decode` modules use strict ByteString instead of lazy one. [#59](https://github.com/kazu-yamamoto/dns/issues/59)+- Default DNS servers are detected automatically on Windows if `RCFilePath` is used. Use vanilla `defaultResolvConf` on Windows! [#83](https://github.com/kazu-yamamoto/dns/pull/83)+- Multiple DNS servers can be used. You can choose either sequential lookup or concurrent lookup. See `resolvConcurrent`. [#81](https://github.com/kazu-yamamoto/dns/pull/81)+- EDNS0 queries are used by default. [#95](https://github.com/kazu-yamamoto/dns/pull/95)+- `lookupSOA` is defined. [#93](https://github.com/kazu-yamamoto/dns/pull/93)+- Cache mechanism is provided. See `resolvCache`.+- Some constructors such as ANY are added in the `Types` module.+- Some bug fixes and code clean-up.++## 2.0.13+- Testing with AppVeyor.+- Detecting a default DNS server on Windows.+- Fixing sendAll on Windows [#72](https://github.com/kazu-yamamoto/dns/pull/72)++## 2.0.12+- Fixing Windows build again++## 2.0.11+- Fixing the StateBinary.get32 parser [#57](https://github.com/kazu-yamamoto/dns/pull/57)+- Removing bytestring-builder dependency [#61](https://github.com/kazu-yamamoto/dns/pull/61)+- Fixing Windows build [#62](https://github.com/kazu-yamamoto/dns/pull/62)++## 2.0.10+- Cleaning up the code. [#47](https://github.com/kazu-yamamoto/dns/pull/47)++## 2.0.9+- Implemented TCP fallback after a truncated UDP response. [#46](https://github.com/kazu-yamamoto/dns/pull/46)++## 2.0.8+- Better handling of encoding and decoding the "root" domain ".". [#45](https://github.com/kazu-yamamoto/dns/pull/45)++## 2.0.7+- Add length checks for A and AAAA records. [#43](https://github.com/kazu-yamamoto/dns/pull/43)++## 2.0.6+- Adding Ord instance. [#41](https://github.com/kazu-yamamoto/dns/pull/41)+- Adding DNSSEC-related RRTYPEs [#40](https://github.com/kazu-yamamoto/dns/pull/40)++## 2.0.5+- Supporting DNS-SEC AD (authenticated data). [#38](https://github.com/kazu-yamamoto/dns/pull/38)+- Removing the dependency to blaze-builder.++## 2.0.4+- Renaming a variable to fix preprocessor conflicts [#37](https://github.com/kazu-yamamoto/dns/pull/37)++## 2.0.3+- Handle invalid opcodes gracefully. [#36](https://github.com/kazu-yamamoto/dns/pull/36)++## 2.0.2+- Providing a new API: decodeMany.++## 2.0.1+- Updating document.++## 2.0.0+- DNSMessage is now monomorphic+- RDATA is now monomorphic+- Removed traversal instance for DNSMessage+- EDNS0 encoding/decoding is now supported+- Removed dnsMapWithType and dnsTraverseWithType functions+- responseA and responseAAAA now take lists of IP addresses as their arguments+- DNSHeader type no longer has qdCount, anCount, nsCount, and arCount fields
Network/DNS.hs view
@@ -1,35 +1,56 @@-{-|- Thread-safe DNS library written in Haskell.-- This code is written in Haskell, not using FFI.-- Sample code for DNS lookup:--@- import qualified Network.DNS as DNS (lookup)- import Network.DNS hiding (lookup)- main :: IO ()- main = do- rs <- makeResolvSeed defaultResolvConf- withResolver rs $ \\resolver -> do- DNS.lookup resolver \"www.example.com\" A >>= print-@--}-+-- | A thread-safe DNS library for both clients and servers written+-- in pure Haskell.+-- The Network.DNS module re-exports all other exposed modules for+-- convenience.+-- Applications will most likely use the high-level interface, while+-- library/daemon authors may need to use the lower-level one.+-- EDNS and TCP fallback are supported.+--+-- Examples:+--+-- >>> :set -XOverloadedStrings+-- >>> rs <- makeResolvSeed defaultResolvConf+-- >>> withResolver rs $ \resolver -> lookupA resolver "192.0.2.1.nip.io"+-- Right [192.0.2.1] module Network.DNS ( -- * High level module Network.DNS.Lookup+ -- | This module contains simple functions to+ -- perform various DNS lookups. If you simply want to resolve a+ -- hostname ('lookupA'), or find a domain's MX record+ -- ('lookupMX'), this is the easiest way to do it.+ , module Network.DNS.Resolver+ -- | Resolver related data types.+ , module Network.DNS.Types+ -- | All of the types that the other modules use.++ , module Network.DNS.Utils+ -- | This module contains utility functions used+ -- for processing DNS data.++ -- * Middle level+ , module Network.DNS.LookupRaw+ -- | This provides the 'lookup', 'lookupAuth', 'lookupRaw' and+ -- 'lookupRawCtl' functions for any resource records.+ -- * Low level- , module Network.DNS.Decode , module Network.DNS.Encode- ) where+ -- | Encoding a query or response. + , module Network.DNS.Decode+ -- | Decoding a qurey or response.++ , module Network.DNS.IO+ -- | Sending and receiving.+) where++import Network.DNS.Decode+import Network.DNS.Encode+import Network.DNS.IO import Network.DNS.Lookup+import Network.DNS.LookupRaw import Network.DNS.Resolver import Network.DNS.Types-import Network.DNS.Decode-import Network.DNS.Encode--+import Network.DNS.Utils
Network/DNS/Decode.hs view
@@ -1,185 +1,103 @@-{-# LANGUAGE OverloadedStrings #-}-+-- | DNS message decoders.+--+-- When in doubt, use the 'decodeAt' or 'decodeManyAt' functions, which+-- correctly handle /circle-arithmetic/ DNS timestamps, e.g., in @RRSIG@+-- resource records. The 'decode', and 'decodeMany' functions are only+-- appropriate in pure contexts when the current time is not available, and+-- @RRSIG@ records are not expected or desired.+--+-- The 'decodeMany' and 'decodeManyAt' functions decode a buffer holding one or+-- more messages, each preceded by 16-bit length in network byte order. This+-- encoding is generally only appropriate for DNS TCP, and because TCP does not+-- preserve message boundaries, the decode is prepared to return a trailing+-- message fragment to be completed and retried when more input arrives from+-- network.+-- module Network.DNS.Decode (- decode- , receive+ -- * Decoding a single DNS message+ decodeAt+ , decode+ -- * Decoding multple length-encoded DNS messages,+ -- e.g., from TCP traffic.+ , decodeManyAt+ , decodeMany ) where -import Control.Applicative-import Control.Monad-import Data.Bits-import Data.ByteString (ByteString)-import qualified Data.ByteString.Char8 as BS-import qualified Data.ByteString.Lazy as BL-import Data.Conduit-import Data.Conduit.Network-import Data.IP-import Data.Maybe-import Network-import Network.DNS.Internal+import qualified Data.ByteString as B++import Network.DNS.Decode.Parsers+import Network.DNS.Imports import Network.DNS.StateBinary+import Network.DNS.Types.Internal ---------------------------------------------------------------- -{-| Receiving DNS data from 'Socket' and parse it.- The second argument is a buffer size for the socket.--}-receive :: Socket -> IO DNSFormat-receive sock = receiveDNSFormat $ sourceSocket sock+-- | Decode an input buffer containing a single encoded DNS message. If the+-- input buffer has excess content beyond the end of the message an error is+-- returned. DNS /circle-arithmetic/ timestamps (e.g. in RRSIG records) are+-- interpreted at the supplied epoch time.+--+decodeAt :: Int64 -- ^ current epoch time+ -> ByteString -- ^ encoded input buffer+ -> Either DNSError DNSMessage -- ^ decoded message or error+decodeAt t bs = fst <$> runSGetAt t (fitSGet (B.length bs) getResponse) bs -----------------------------------------------------------------+-- | Decode an input buffer containing a single encoded DNS message. If the+-- input buffer has excess content beyond the end of the message an error is+-- returned. DNS /circle-arithmetic/ timestamps (e.g. in RRSIG records) are+-- interpreted based on a nominal time in the year 2073 chosen to maximize+-- the time range for which this gives correct translations of 32-bit epoch+-- times to absolute 64-bit epoch times. This will yield incorrect results+-- starting circa 2141.+--+decode :: ByteString -- ^ encoded input buffer+ -> Either DNSError DNSMessage -- ^ decoded message or error+decode bs = fst <$> runSGet (fitSGet (B.length bs) getResponse) bs -{-| Parsing DNS data.--}-decode :: BL.ByteString -> Either String DNSFormat-decode bs = fst <$> runSGet decodeResponse bs+-- | Decode a buffer containing multiple encoded DNS messages each preceded by+-- a 16-bit length in network byte order. Any left-over bytes of a partial+-- message after the last complete message are returned as the second element+-- of the result tuple. DNS /circle-arithmetic/ timestamps (e.g. in RRSIG+-- records) are interpreted at the supplied epoch time.+--+decodeManyAt :: Int64 -- ^ current epoch time+ -> ByteString -- ^ encoded input buffer+ -> Either DNSError ([DNSMessage], ByteString)+ -- ^ decoded messages and left-over partial message+ -- or error if any complete message fails to parse.+decodeManyAt t bs = decodeMParse (decodeAt t) bs ------------------------------------------------------------------receiveDNSFormat :: Source (ResourceT IO) ByteString -> IO DNSFormat-receiveDNSFormat src = fst <$> runResourceT (src $$ sink)- where- sink = sinkSGet decodeResponse+-- | Decode a buffer containing multiple encoded DNS messages each preceded by+-- a 16-bit length in network byte order. Any left-over bytes of a partial+-- message after the last complete message are returned as the second element+-- of the result tuple. DNS /circle-arithmetic/ timestamps (e.g. in RRSIG+-- records) are interpreted based on a nominal time in the year 2078 chosen to+-- give correct dates for DNS timestamps over a 136 year time range from the+-- date the root zone was signed on the 15th of July 2010 until the 21st of+-- August in 2146. Outside this date range the output is off by some non-zero+-- multiple 2\^32 seconds.+--+decodeMany :: ByteString -- ^ encoded input buffer+ -> Either DNSError ([DNSMessage], ByteString)+ -- ^ decoded messages and left-over partial message+ -- or error if any complete message fails to parse.+decodeMany bs = decodeMParse decode bs ----------------------------------------------------------------- -decodeResponse :: SGet DNSFormat-decodeResponse = do- hd <- decodeHeader- DNSFormat hd <$> decodeQueries (qdCount hd)- <*> decodeRRs (anCount hd)- <*> decodeRRs (nsCount hd)- <*> decodeRRs (arCount hd)--------------------------------------------------------------------decodeFlags :: SGet DNSFlags-decodeFlags = toFlags <$> get16- where- toFlags flgs = DNSFlags (getQorR flgs)- (getOpcode flgs)- (getAuthAnswer flgs)- (getTrunCation flgs)- (getRecDesired flgs)- (getRecAvailable flgs)- (getRcode flgs)- getQorR w = if testBit w 15 then QR_Response else QR_Query- getOpcode w = toEnum $ fromIntegral $ shiftR w 11 .&. 0x0f- getAuthAnswer w = testBit w 10- getTrunCation w = testBit w 9- getRecDesired w = testBit w 8- getRecAvailable w = testBit w 7- getRcode w = toEnum $ fromIntegral $ w .&. 0x0f--------------------------------------------------------------------decodeHeader :: SGet DNSHeader-decodeHeader = DNSHeader <$> decodeIdentifier- <*> decodeFlags- <*> decodeQdCount- <*> decodeAnCount- <*> decodeNsCount- <*> decodeArCount- where- decodeIdentifier = getInt16- decodeQdCount = getInt16- decodeAnCount = getInt16- decodeNsCount = getInt16- decodeArCount = getInt16--------------------------------------------------------------------decodeQueries :: Int -> SGet [Question]-decodeQueries n = replicateM n decodeQuery--decodeType :: SGet TYPE-decodeType = intToType <$> getInt16--decodeQuery :: SGet Question-decodeQuery = Question <$> decodeDomain- <*> (decodeType <* ignoreClass)--decodeRRs :: Int -> SGet [ResourceRecord]-decodeRRs n = replicateM n decodeRR--decodeRR :: SGet ResourceRecord-decodeRR = do- Question dom typ <- decodeQuery- ttl <- decodeTTL- len <- decodeRLen- dat <- decodeRData typ len- return ResourceRecord { rrname = dom- , rrtype = typ- , rrttl = ttl- , rdlen = len- , rdata = dat- }- where- decodeTTL = fromIntegral <$> get32- decodeRLen = getInt16--decodeRData :: TYPE -> Int -> SGet RDATA-decodeRData NS _ = RD_NS <$> decodeDomain-decodeRData MX _ = RD_MX <$> decodePreference <*> decodeDomain- where- decodePreference = getInt16-decodeRData CNAME _ = RD_CNAME <$> decodeDomain-decodeRData TXT len = (RD_TXT . ignoreLength) <$> getNByteString len- where- ignoreLength = BS.tail-decodeRData A len = (RD_A . toIPv4) <$> getNBytes len-decodeRData AAAA len = (RD_AAAA . toIPv6 . combine) <$> getNBytes len- where- combine [] = []- combine [_] = error "combine"- combine (a:b:cs) = a * 256 + b : combine cs-decodeRData SOA _ = RD_SOA <$> decodeDomain- <*> decodeDomain- <*> decodeSerial- <*> decodeRefesh- <*> decodeRetry- <*> decodeExpire- <*> decodeMinumun- where- decodeSerial = getInt32- decodeRefesh = getInt32- decodeRetry = getInt32- decodeExpire = getInt32- decodeMinumun = getInt32-decodeRData PTR _ = RD_PTR <$> decodeDomain-decodeRData SRV _ = RD_SRV <$> decodePriority- <*> decodeWeight- <*> decodePort- <*> decodeDomain- where- decodePriority = getInt16- decodeWeight = getInt16- decodePort = getInt16--decodeRData _ len = RD_OTH <$> getNBytes len--------------------------------------------------------------------decodeDomain :: SGet Domain-decodeDomain = do- pos <- getPosition- c <- getInt8- if c == 0 then- return ""- else do- let n = getValue c- if isPointer c then do- d <- getInt8- let offset = n * 256 + d- fromMaybe (error $ "decodeDomain: " ++ show offset) <$> pop offset- else do- hs <- getNByteString n- ds <- decodeDomain- let dom = hs `BS.append` "." `BS.append` ds- push pos dom- return dom+-- | Decode multiple messages using the given parser.+--+decodeMParse :: (ByteString -> Either DNSError DNSMessage)+ -- ^ message decoder+ -> ByteString+ -- ^ enoded input buffer+ -> Either DNSError ([DNSMessage], ByteString)+ -- ^ decoded messages and left-over partial message+ -- or error if any complete message fails to parse.+decodeMParse decoder bs = do+ ((bss, _), leftovers) <- runSGetWithLeftovers lengthEncoded bs+ msgs <- mapM decoder bss+ return (msgs, leftovers) where- getValue c = c .&. 0x3f- isPointer c = testBit c 7 && testBit c 6--ignoreClass :: SGet ()-ignoreClass = () <$ get16+ -- Read a list of length-encoded bytestrings+ lengthEncoded :: SGet [ByteString]+ lengthEncoded = many $ getInt16 >>= getNByteString
Network/DNS/Encode.hs view
@@ -1,167 +1,34 @@-{-# LANGUAGE RecordWildCards #-}+-- | DNS message encoder.+--+-- Note: 'Nework.DNS' is a client library, and its focus is on /sending/+-- /queries/, and /receiving/ /replies/. Thefore, while this module is+-- reasonably adept at query generation, building a DNS server with this+-- module requires additional work to handle message size limits, correct UDP+-- truncation, proper EDNS negotiation, and so on. Support for server-side DNS+-- is at best rudimentary.+--+-- For sending queries, in most cases you should be using one of the functions+-- from 'Network.DNS.Lookup' and 'Network.DNS.LookupRaw', or lastly, if you+-- want to handle the network reads and writes for yourself (with your own code+-- for UDP retries, TCP fallback, EDNS fallback, ...), then perhaps+-- 'Network.DNS.IO.encodeQuestion' (letting 'Network.DNS' do the lookups for+-- you in an @async@ thread is likely much simpler).+-- module Network.DNS.Encode (+ -- * Encode a DNS query (or response). encode- , composeQuery ) where -import qualified Blaze.ByteString.Builder as BB (toByteString, fromWrite, writeInt16be)-import qualified Data.ByteString.Lazy.Char8 as BL (ByteString)-import qualified Data.ByteString.Char8 as BS (length, null, break, drop)+import Network.DNS.Imports import Network.DNS.StateBinary-import Network.DNS.Internal-import Data.Monoid-import Control.Monad.State-import Data.Bits-import Data.Word-import Data.IP--(+++) :: Monoid a => a -> a -> a-(+++) = mappend--------------------------------------------------------------------{-| Composing query. First argument is a number to identify response.--}-composeQuery :: Int -> [Question] -> BL.ByteString-composeQuery idt qs = encode qry- where- hdr = header defaultQuery- qry = defaultQuery {- header = hdr {- identifier = idt- , qdCount = length qs- }- , question = qs- }--------------------------------------------------------------------{-| Composing DNS data.--}-encode :: DNSFormat -> BL.ByteString-encode fmt = runSPut (encodeDNSFormat fmt)--------------------------------------------------------------------encodeDNSFormat :: DNSFormat -> SPut-encodeDNSFormat fmt = encodeHeader hdr- +++ mconcat (map encodeQuestion qs)- +++ mconcat (map encodeRR an)- +++ mconcat (map encodeRR au)- +++ mconcat (map encodeRR ad)- where- hdr = header fmt- qs = question fmt- an = answer fmt- au = authority fmt- ad = additional fmt--encodeHeader :: DNSHeader -> SPut-encodeHeader hdr = encodeIdentifier (identifier hdr)- +++ encodeFlags (flags hdr)- +++ decodeQdCount (qdCount hdr)- +++ decodeAnCount (anCount hdr)- +++ decodeNsCount (nsCount hdr)- +++ decodeArCount (arCount hdr)- where- encodeIdentifier = putInt16- decodeQdCount = putInt16- decodeAnCount = putInt16- decodeNsCount = putInt16- decodeArCount = putInt16--encodeFlags :: DNSFlags -> SPut-encodeFlags DNSFlags{..} = put16 word- where- word16 :: Enum a => a -> Word16- word16 = toEnum . fromEnum-- set :: Word16 -> State Word16 ()- set byte = modify (.|. byte)-- st :: State Word16 ()- st = sequence_- [ set (word16 rcode)- , when recAvailable $ set (bit 7)- , when recDesired $ set (bit 8)- , when trunCation $ set (bit 9)- , when authAnswer $ set (bit 10)- , set (word16 opcode `shiftL` 11)- , when (qOrR==QR_Response) $ set (bit 15)- ]-- word = execState st 0--encodeQuestion :: Question -> SPut-encodeQuestion Question{..} =- encodeDomain qname- +++ putInt16 (typeToInt qtype)- +++ put16 1--encodeRR :: ResourceRecord -> SPut-encodeRR ResourceRecord{..} =- mconcat- [ encodeDomain rrname- , putInt16 (typeToInt rrtype)- , put16 1- , putInt32 rrttl- , rlenRDATA- ]- where- -- Encoding rdata without using rdlen- rlenRDATA = do- addPositionW 2 -- "simulate" putInt16- rDataWrite <- encodeRDATA rdata- let rdataLength = fromIntegral . BS.length . BB.toByteString . BB.fromWrite $ rDataWrite- let rlenWrite = BB.writeInt16be rdataLength- return rlenWrite +++ return rDataWrite--encodeRDATA :: RDATA -> SPut-encodeRDATA rd = case rd of- (RD_A ip) -> mconcat $ map putInt8 (fromIPv4 ip)- (RD_AAAA ip) -> mconcat $ map putInt16 (fromIPv6 ip)- (RD_NS dom) -> encodeDomain dom- (RD_CNAME dom) -> encodeDomain dom- (RD_PTR dom) -> encodeDomain dom- (RD_MX prf dom) -> mconcat [putInt16 prf, encodeDomain dom]- (RD_TXT txt) -> putByteString txt- (RD_OTH bytes) -> mconcat $ map putInt8 bytes- (RD_SOA d1 d2 serial refresh retry expire min') -> mconcat- [ encodeDomain d1- , encodeDomain d2- , putInt32 serial- , putInt32 refresh- , putInt32 retry- , putInt32 expire- , putInt32 min'- ]- (RD_SRV prio weight port dom) -> mconcat- [ putInt16 prio- , putInt16 weight- , putInt16 port- , encodeDomain dom- ]--------------------------------------------------------------------encodeDomain :: Domain -> SPut-encodeDomain dom | BS.null dom = put8 0-encodeDomain dom = do- mpos <- wsPop dom- cur <- gets wsPosition- case mpos of- Just pos -> encodePointer pos- Nothing -> wsPush dom cur >>- mconcat [ encodePartialDomain hd- , encodeDomain tl- ]- where- (hd, tl') = BS.break (=='.') dom- tl = if BS.null tl' then tl' else BS.drop 1 tl'--encodePointer :: Int -> SPut-encodePointer pos = let w = (pos .|. 0xc000) in putInt16 w+import Network.DNS.Types.Internal+import Network.DNS.Encode.Builders -encodePartialDomain :: Domain -> SPut-encodePartialDomain sub = putInt8 (BS.length sub)- +++ putByteString sub+-- | Encode a 'DNSMessage' for transmission over UDP. For transmission over+-- TCP encapsulate the result via 'Network.DNS.IO.encodeVC', or use+-- 'Network.DNS.IO.sendVC', which handles this internally. If any+-- 'ResourceRecord' in the message contains incorrectly encoded 'Domain' name+-- ByteStrings, this function may raise a 'DecodeError'.+--+encode :: DNSMessage -> ByteString+encode = runSPut . putDNSMessage
+ Network/DNS/IO.hs view
@@ -0,0 +1,201 @@+{-# LANGUAGE OverloadedStrings #-}++module Network.DNS.IO (+ -- * Receiving DNS messages+ receive+ , receiveFrom+ , receiveVC+ -- * Sending pre-encoded messages+ , send+ , sendTo+ , sendVC+ , sendAll+ -- ** Encoding queries for transmission+ , encodeQuestion+ , encodeVC+ -- ** Creating query response messages+ , responseA+ , responseAAAA+ ) where++import qualified Control.Exception as E+import qualified Data.ByteString as B+import qualified Data.ByteString.Builder as BB+import qualified Data.ByteString.Char8 as BS+import qualified Data.ByteString.Lazy.Char8 as LBS+import Data.IP (IPv4, IPv6)+import Time.System (timeCurrent)+import Time.Types (Elapsed(..), Seconds(..))+import Network.Socket (Socket, SockAddr)+import Network.Socket.ByteString (recv, recvFrom)+import qualified Network.Socket.ByteString as Socket+import System.IO.Error++import Network.DNS.Decode (decodeAt)+import Network.DNS.Encode (encode)+import Network.DNS.Imports+import Network.DNS.Types.Internal++----------------------------------------------------------------++-- | Receive and decode a single 'DNSMessage' from a UDP 'Socket', throwing away+-- the client address. Messages longer than 'maxUdpSize' are silently+-- truncated, but this should not occur in practice, since we cap the advertised+-- EDNS UDP buffer size limit at the same value. A 'DNSError' is raised if I/O+-- or message decoding fails.+--+receive :: Socket -> IO DNSMessage+receive sock = do+ let bufsiz = fromIntegral maxUdpSize+ bs <- recv sock bufsiz `E.catch` \e -> E.throwIO $ NetworkFailure e+ Elapsed (Seconds now) <- timeCurrent+ case decodeAt now bs of+ Left e -> E.throwIO e+ Right msg -> return msg++-- | Receive and decode a single 'DNSMessage' from a UDP 'Socket'. Messages+-- longer than 'maxUdpSize' are silently truncated, but this should not occur+-- in practice, since we cap the advertised EDNS UDP buffer size limit at the+-- same value. A 'DNSError' is raised if I/O or message decoding fails.+--+receiveFrom :: Socket -> IO (DNSMessage, SockAddr)+receiveFrom sock = do+ let bufsiz = fromIntegral maxUdpSize+ (bs, client) <- recvFrom sock bufsiz `E.catch` \e -> E.throwIO $ NetworkFailure e+ Elapsed (Seconds now) <- timeCurrent+ case decodeAt now bs of+ Left e -> E.throwIO e+ Right msg -> return (msg, client)++-- | Receive and decode a single 'DNSMesage' from a virtual-circuit (TCP). It+-- is up to the caller to implement any desired timeout. An 'DNSError' is+-- raised if I/O or message decoding fails.+--+receiveVC :: Socket -> IO DNSMessage+receiveVC sock = do+ len <- toLen <$> recvDNS sock 2+ bs <- recvDNS sock len+ Elapsed (Seconds now) <- timeCurrent+ case decodeAt now bs of+ Left e -> E.throwIO e+ Right msg -> return msg+ where+ toLen bs = case B.unpack bs of+ [hi, lo] -> 256 * (fromIntegral hi) + (fromIntegral lo)+ _ -> 0 -- never reached++recvDNS :: Socket -> Int -> IO ByteString+recvDNS sock len = recv1 `E.catch` \e -> E.throwIO $ NetworkFailure e+ where+ recv1 = do+ bs1 <- recvCore len+ if BS.length bs1 == len then+ return bs1+ else do+ loop bs1+ loop bs0 = do+ let left = len - BS.length bs0+ bs1 <- recvCore left+ let bs = bs0 `BS.append` bs1+ if BS.length bs == len then+ return bs+ else+ loop bs+ eofE = mkIOError eofErrorType "connection terminated" Nothing Nothing+ recvCore len0 = do+ bs <- recv sock len0+ if bs == "" then+ E.throwIO eofE+ else+ return bs++----------------------------------------------------------------++-- | Send an encoded 'DNSMessage' datagram over UDP. The message length is+-- implicit in the size of the UDP datagram. With TCP you must use 'sendVC',+-- because TCP does not have message boundaries, and each message needs to be+-- prepended with an explicit length. The socket must be explicitly connected+-- to the destination nameserver.+--+send :: Socket -> ByteString -> IO ()+send = (void .). Socket.send+{-# INLINE send #-}++-- | Send an encoded 'DNSMessage' datagram over UDP to a given address. The+-- message length is implicit in the size of the UDP datagram. With TCP you+-- must use 'sendVC', because TCP does not have message boundaries, and each+-- message needs to be prepended with an explicit length.+--+sendTo :: Socket -> ByteString -> SockAddr -> IO ()+sendTo sock str addr = Socket.sendTo sock str addr >> return ()+{-# INLINE sendTo #-}++-- | Send a single encoded 'DNSMessage' over TCP. An explicit length is+-- prepended to the encoded buffer before transmission. If you want to+-- send a batch of multiple encoded messages back-to-back over a single+-- TCP connection, and then loop to collect the results, use 'encodeVC'+-- to prefix each message with a length, and then use 'sendAll' to send+-- a concatenated batch of the resulting encapsulated messages.+--+sendVC :: Socket -> ByteString -> IO ()+sendVC = (. encodeVC). sendAll+{-# INLINE sendVC #-}++-- | Send one or more encoded 'DNSMessage' buffers over TCP, each allready+-- encapsulated with an explicit length prefix (perhaps via 'encodeVC') and+-- then concatenated into a single buffer. DO NOT use 'sendAll' with UDP.+--+sendAll :: Socket -> BS.ByteString -> IO ()+sendAll = Socket.sendAll+{-# INLINE sendAll #-}++-- | The encoded 'DNSMessage' has the specified request ID. The default values+-- of the RD, AD, CD and DO flag bits, as well as various EDNS features, can be+-- adjusted via the 'QueryControls' parameter.+--+-- The caller is responsible for generating the ID via a securely seeded+-- CSPRNG.+--+encodeQuestion :: Identifier -- ^ Crypto random request id+ -> Question -- ^ Query name and type+ -> QueryControls -- ^ Query flag and EDNS overrides+ -> ByteString+encodeQuestion idt q ctls = encode $ makeQuery idt q ctls++-- | Encapsulate an encoded 'DNSMessage' buffer for transmission over a TCP+-- virtual circuit. With TCP the buffer needs to start with an explicit+-- length (the length is implicit with UDP).+--+encodeVC :: ByteString -> ByteString+encodeVC legacyQuery =+ let len = LBS.toStrict . BB.toLazyByteString $ BB.int16BE $ fromIntegral $ BS.length legacyQuery+ in len <> legacyQuery+{-# INLINE encodeVC #-}++----------------------------------------------------------------++-- | Compose a response with a single IPv4 RRset. If the query+-- had an EDNS pseudo-header, a suitable EDNS pseudo-header must+-- be added to the response message, or else a 'FormatErr' response+-- must be sent. The response TTL defaults to 300 seconds, and+-- should be updated (to the same value across all the RRs) if some+-- other TTL value is more appropriate.+--+responseA :: Identifier -> Question -> [IPv4] -> DNSMessage+responseA idt q ips = makeResponse idt q as+ where+ dom = qname q+ as = ResourceRecord dom A classIN 300 . RD_A <$> ips++-- | Compose a response with a single IPv6 RRset. If the query+-- had an EDNS pseudo-header, a suitable EDNS pseudo-header must+-- be added to the response message, or else a 'FormatErr' response+-- must be sent. The response TTL defaults to 300 seconds, and+-- should be updated (to the same value across all the RRs) if some+-- other TTL value is more appropriate.+--+responseAAAA :: Identifier -> Question -> [IPv6] -> DNSMessage+responseAAAA idt q ips = makeResponse idt q as+ where+ dom = qname q+ as = ResourceRecord dom AAAA classIN 300 . RD_AAAA <$> ips
− Network/DNS/Internal.hs
@@ -1,208 +0,0 @@-module Network.DNS.Internal where--import Data.ByteString (ByteString)-import qualified Data.ByteString.Char8 as BS-import Data.Char-import Data.IP-import Data.Maybe--------------------------------------------------------------------{-|- Type for domain.--}-type Domain = ByteString--------------------------------------------------------------------{-|- Types for resource records.--}-data TYPE = A | AAAA | NS | TXT | MX | CNAME | SOA | PTR | SRV- | UNKNOWN Int deriving (Eq, Show, Read)--rrDB :: [(TYPE, Int)]-rrDB = [- (A, 1)- , (NS, 2)- , (CNAME, 5)- , (SOA, 6)- , (PTR, 12)- , (MX, 15)- , (TXT, 16)- , (AAAA, 28)- , (SRV, 33)- ]--rookup :: (Eq b) => b -> [(a,b)] -> Maybe a-rookup _ [] = Nothing-rookup key ((x,y):xys)- | key == y = Just x- | otherwise = rookup key xys--intToType :: Int -> TYPE-intToType n = fromMaybe (UNKNOWN n) $ rookup n rrDB-typeToInt :: TYPE -> Int-typeToInt (UNKNOWN x) = x-typeToInt t = fromMaybe 0 $ lookup t rrDB--toType :: String -> TYPE-toType = read . map toUpper--------------------------------------------------------------------{-|- Raw data format for DNS Query and Response.--}-data DNSFormat = DNSFormat {- header :: DNSHeader- , question :: [Question]- , answer :: [ResourceRecord]- , authority :: [ResourceRecord]- , additional :: [ResourceRecord]- } deriving (Eq, Show)--{-|- Raw data format for the header of DNS Query and Response.--}-data DNSHeader = DNSHeader {- identifier :: Int- , flags :: DNSFlags- , qdCount :: Int- , anCount :: Int- , nsCount :: Int- , arCount :: Int- } deriving (Eq, Show)--{-|- Raw data format for the flags of DNS Query and Response.--}-data DNSFlags = DNSFlags {- qOrR :: QorR- , opcode :: OPCODE- , authAnswer :: Bool- , trunCation :: Bool- , recDesired :: Bool- , recAvailable :: Bool- , rcode :: RCODE- } deriving (Eq, Show)--------------------------------------------------------------------data QorR = QR_Query | QR_Response deriving (Eq, Show)--data OPCODE = OP_STD | OP_INV | OP_SSR deriving (Eq, Show, Enum)--data RCODE = NoErr | FormatErr | ServFail | NameErr | NotImpl | Refused deriving (Eq, Show, Enum)--------------------------------------------------------------------{-|- Raw data format for DNS questions.--}-data Question = Question {- qname :: Domain- , qtype :: TYPE- } deriving (Eq, Show)--{-|- Making "Question".--}-makeQuestion :: Domain -> TYPE -> Question-makeQuestion = Question--------------------------------------------------------------------{-|- Raw data format for resource records.--}-data ResourceRecord = ResourceRecord {- rrname :: Domain- , rrtype :: TYPE- , rrttl :: Int- , rdlen :: Int- , rdata :: RDATA- } deriving (Eq, Show)--{-|- Raw data format for each type.--}-data RDATA = RD_NS Domain | RD_CNAME Domain | RD_MX Int Domain | RD_PTR Domain- | RD_SOA Domain Domain Int Int Int Int Int- | RD_A IPv4 | RD_AAAA IPv6 | RD_TXT ByteString- | RD_SRV Int Int Int Domain- | RD_OTH [Int] deriving (Eq)--instance Show RDATA where- show (RD_NS dom) = BS.unpack dom- show (RD_MX prf dom) = BS.unpack dom ++ " " ++ show prf- show (RD_CNAME dom) = BS.unpack dom- show (RD_A a) = show a- show (RD_AAAA aaaa) = show aaaa- show (RD_TXT txt) = BS.unpack txt- show (RD_SOA mn _ _ _ _ _ mi) = BS.unpack mn ++ " " ++ show mi- show (RD_PTR dom) = BS.unpack dom- show (RD_SRV pri wei prt dom) = show pri ++ " " ++ show wei ++ " " ++ show prt ++ BS.unpack dom- show (RD_OTH is) = show is--------------------------------------------------------------------defaultQuery :: DNSFormat-defaultQuery = DNSFormat {- header = DNSHeader {- identifier = 0- , flags = DNSFlags {- qOrR = QR_Query- , opcode = OP_STD- , authAnswer = False- , trunCation = False- , recDesired = True- , recAvailable = False- , rcode = NoErr- }- , qdCount = 0- , anCount = 0- , nsCount = 0- , arCount = 0- }- , question = []- , answer = []- , authority = []- , additional = []- }--defaultResponse :: DNSFormat-defaultResponse =- let hd = header defaultQuery- flg = flags hd- in defaultQuery {- header = hd {- flags = flg {- qOrR = QR_Response- , authAnswer = True- , recAvailable = True- }- }- }--responseA :: Int -> Question -> IPv4 -> DNSFormat-responseA ident q ip =- let hd = header defaultResponse- dom = qname q- an = ResourceRecord dom A 300 4 (RD_A ip)- in defaultResponse {- header = hd { identifier=ident, qdCount = 1, anCount = 1 }- , question = [q]- , answer = [an]- }--responseAAAA :: Int -> Question -> IPv6 -> DNSFormat-responseAAAA ident q ip =- let hd = header defaultResponse- dom = qname q- an = ResourceRecord dom AAAA 300 16 (RD_AAAA ip)- in defaultResponse {- header = hd { identifier=ident, qdCount = 1, anCount = 1 }- , question = [q]- , answer = [an]- }
Network/DNS/Lookup.hs view
@@ -1,123 +1,439 @@-{-|- Upper level DNS lookup functions.--}-+-- | Simple, high-level DNS lookup functions for clients.+--+-- All of the lookup functions necessary run in IO since they+-- interact with the network. The return types are similar, but+-- differ in what can be returned from a successful lookup.+--+-- We can think of the return type as either \"what I asked for\" or+-- \"an error\". For example, the 'lookupA' function, if successful,+-- will return a list of 'IPv4'. The 'lookupMX' function will+-- instead return a list of @('Domain','Int')@ pairs, where each pair+-- represents a hostname and its associated priority.+--+-- The order of multiple results may not be consistent between+-- lookups. If you require consistent results, apply+-- 'Data.List.sort' to the returned list.+--+-- The errors that can occur are the same for all lookups. Namely:+--+-- * Timeout+--+-- * Wrong sequence number (foul play?)+--+-- * Unexpected data in the response+--+-- If an error occurs, you should be able to pattern match on the+-- 'DNSError' constructor to determine which of these is the case.+--+-- /Note/: A result of \"no records\" is not considered an+-- error. If you perform, say, an \'AAAA\' lookup for a domain with+-- no such records, the \"success\" result would be @Right []@.+--+-- We perform a successful lookup of \"www.example.com\":+--+-- >>> let hostname = Data.ByteString.Char8.pack "www.example.com"+-- >>>+-- >>> rs <- makeResolvSeed defaultResolvConf+-- >>> withResolver rs $ \resolver -> lookupA resolver hostname+-- Right [93.184.216.34]+--+-- The only error that we can easily cause is a timeout. We do this+-- by creating and utilizing a 'ResolvConf' which has a timeout of+-- one millisecond and a very limited number of retries:+--+-- >>> let hostname2 = Data.ByteString.Char8.pack "www.example.com"+-- >>> let badrc = defaultResolvConf { resolvTimeout = 0, resolvRetry = 1 }+-- >>>+-- >>> rs2 <- makeResolvSeed badrc+-- >>> withResolver rs2 $ \resolver -> lookupA resolver hostname2+-- Left RetryLimitExceeded+--+-- As is the convention, successful results will always be wrapped+-- in a 'Right' while errors will be wrapped in a 'Left'.+--+-- For convenience, you may wish to enable GHC\'s OverloadedStrings+-- extension. This will allow you to avoid calling+-- 'Data.ByteString.Char8.pack' on each domain name. See+-- <https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#overloaded-string-literals>+-- for more information. In the following examples,+-- we assuem this extension is enabled.+--+-- All lookup functions eventually call 'lookupRaw'. See its documentation+-- to understand the concrete lookup behavior. module Network.DNS.Lookup ( lookupA, lookupAAAA , lookupMX, lookupAviaMX, lookupAAAAviaMX , lookupNS+ , lookupNSAuth , lookupTXT+ , lookupSOA , lookupPTR+ , lookupRDNS , lookupSRV ) where -import Control.Applicative-import Data.ByteString (ByteString)-import Data.IP-import Data.Maybe+import qualified Data.ByteString.Char8 as BS+import Data.IP (IPv4, IPv6)++import Network.DNS.Imports+import Network.DNS.LookupRaw as DNS import Network.DNS.Resolver as DNS-import Network.DNS.Types+import Network.DNS.Types.Internal +-- $setup+-- >>> :set -XOverloadedStrings+ ---------------------------------------------------------------- -{-|- Resolving 'IPv4' by 'A'.--}-lookupA :: Resolver -> Domain -> IO (Maybe [IPv4])-lookupA rlv dom = toV4 <$> DNS.lookup rlv dom A+-- | Look up all \'A\' records for the given hostname.+--+-- A straightforward example:+--+-- >>> rs <- makeResolvSeed defaultResolvConf+-- >>> withResolver rs $ \resolver -> lookupA resolver "192.0.2.1.nip.io"+-- Right [192.0.2.1]+--+-- This function will also follow a CNAME and resolve its target if+-- one exists for the queried hostname:+--+-- >>> rs2 <- makeResolvSeed defaultResolvConf+-- >>> withResolver rs2 $ \resolver -> lookupA resolver "www.kame.net"+-- Right [210.155.141.200]+--+lookupA :: Resolver -> Domain -> IO (Either DNSError [IPv4])+lookupA rlv dom = do+ erds <- DNS.lookup rlv dom A+ case erds of+ -- See lookupXviaMX for an explanation of this construct.+ Left err -> return (Left err)+ Right rds -> return $ mapM unTag rds where- toV4 = fmap (map unTag)- unTag (RD_A x) = x- unTag _ = error "lookupA"+ unTag :: RData -> Either DNSError IPv4+ unTag (RD_A x) = Right x+ unTag _ = Left UnexpectedRDATA -{-|- Resolving 'IPv6' by 'AAAA'.--}-lookupAAAA :: Resolver -> Domain -> IO (Maybe [IPv6])-lookupAAAA rlv dom = toV6 <$> DNS.lookup rlv dom AAAA++-- | Look up all (IPv6) \'AAAA\' records for the given hostname.+--+-- Examples:+--+-- >>> rs <- makeResolvSeed defaultResolvConf+-- >>> withResolver rs $ \resolver -> lookupAAAA resolver "www.wide.ad.jp"+-- Right [2001:200:0:180c:20c:29ff:fec9:9d61]+--+lookupAAAA :: Resolver -> Domain -> IO (Either DNSError [IPv6])+lookupAAAA rlv dom = do+ erds <- DNS.lookup rlv dom AAAA+ case erds of+ -- See lookupXviaMX for an explanation of this construct.+ Left err -> return (Left err)+ Right rds -> return $ mapM unTag rds where- toV6 = fmap (map unTag)- unTag (RD_AAAA x) = x- unTag _ = error "lookupAAAA"+ unTag :: RData -> Either DNSError IPv6+ unTag (RD_AAAA x) = Right x+ unTag _ = Left UnexpectedRDATA ---------------------------------------------------------------- -{-|- Resolving 'Domain' and its preference by 'MX'.--}-lookupMX :: Resolver -> Domain -> IO (Maybe [(Domain,Int)])-lookupMX rlv dom = toMX <$> DNS.lookup rlv dom MX+-- | Look up all \'MX\' records for the given hostname. Two parts+-- constitute an MX record: a hostname , and an integer priority. We+-- therefore return each record as a @('Domain', Int)@.+--+-- In this first example, we look up the MX for the domain \"example.com\".+-- It has an RFC7505 NULL MX (to prevent a deluge of spam from examples+-- posted on the internet).+--+-- >>> rs <- makeResolvSeed defaultResolvConf+-- >>> withResolver rs $ \resolver -> lookupMX resolver "example.com"+-- Right [(".",0)]+--+--+-- The domain \"mew.org\" does however have a single MX:+--+-- >>> rs2 <- makeResolvSeed defaultResolvConf+-- >>> withResolver rs2 $ \resolver -> lookupMX resolver "mew.org"+-- Right [("mail.mew.org.",10)]+--+-- Also note that all hostnames are returned with a trailing dot to+-- indicate the DNS root.+--+-- However the MX host itself has no need for an MX record, so its MX RRset+-- is empty. But, \"no results\" is still a successful result.+--+-- >>> rs3 <- makeResolvSeed defaultResolvConf+-- >>> withResolver rs3 $ \resolver -> lookupMX resolver "mail.mew.org"+-- Right []+--+lookupMX :: Resolver -> Domain -> IO (Either DNSError [(Domain,Int)])+lookupMX rlv dom = do+ erds <- DNS.lookup rlv dom MX+ case erds of+ -- See lookupXviaMX for an explanation of this construct.+ Left err -> return (Left err)+ Right rds -> return $ mapM unTag rds where- toMX = fmap (map unTag)- unTag (RD_MX pr dm) = (dm,pr)- unTag _ = error "lookupMX"+ unTag :: RData -> Either DNSError (Domain,Int)+ unTag (RD_MX pr dm) = Right (dm, fromIntegral pr)+ unTag _ = Left UnexpectedRDATA -{-|- Resolving 'IPv4' by 'A' via 'MX'.--}-lookupAviaMX :: Resolver -> Domain -> IO (Maybe [IPv4])+-- | Look up all \'MX\' records for the given hostname, and then+-- resolve their hostnames to IPv4 addresses by calling+-- 'lookupA'. The priorities are not retained.+--+-- Examples:+--+-- >>> import Data.List (sort)+-- >>> rs <- makeResolvSeed defaultResolvConf+-- >>> ips <- withResolver rs $ \resolver -> lookupAviaMX resolver "wide.ad.jp"+-- >>> fmap sort ips+-- Right [203.178.136.30]+--+-- Since there is more than one result, it is necessary to sort the+-- list in order to check for equality.+--+lookupAviaMX :: Resolver -> Domain -> IO (Either DNSError [IPv4]) lookupAviaMX rlv dom = lookupXviaMX rlv dom (lookupA rlv) -{-|- Resolving 'IPv6' by 'AAAA' via 'MX'.--}-lookupAAAAviaMX :: Resolver -> Domain -> IO (Maybe [IPv6])+-- | Look up all \'MX\' records for the given hostname, and then+-- resolve their hostnames to IPv6 addresses by calling+-- 'lookupAAAA'. The priorities are not retained.+--+lookupAAAAviaMX :: Resolver -> Domain -> IO (Either DNSError [IPv6]) lookupAAAAviaMX rlv dom = lookupXviaMX rlv dom (lookupAAAA rlv) -lookupXviaMX :: Show a => Resolver -> Domain -> (Domain -> IO (Maybe [a])) -> IO (Maybe [a])+lookupXviaMX :: Resolver+ -> Domain+ -> (Domain -> IO (Either DNSError [a]))+ -> IO (Either DNSError [a]) lookupXviaMX rlv dom func = do- mdps <- lookupMX rlv dom- maybe (return Nothing) lookup' mdps+ edps <- lookupMX rlv dom+ case edps of+ -- We have to deconstruct and reconstruct the error so that the+ -- typechecker does not conclude that a ~ (Domain, Int).+ Left err -> return (Left err)+ Right dps -> do+ -- We'll get back a [Either DNSError a] here.+ responses <- mapM (func . fst) dps+ -- We can use 'sequence' to join all of the Eithers+ -- together. If any of them are (Left _), we'll get a Left+ -- overall. Otherwise, we'll get Right [a].+ let overall = sequence responses+ -- Finally, we use (fmap concat) to concatenate the responses+ -- if there were no errors.+ return $ fmap concat overall++++----------------------------------------------------------------++-- | This function performs the real work for both 'lookupNS' and+-- 'lookupNSAuth'. The only difference between those two is which+-- function, 'lookup' or 'lookupAuth', is used to perform the+-- lookup. We take either of those as our first parameter.+lookupNSImpl :: (Resolver -> Domain -> TYPE -> IO (Either DNSError [RData]))+ -> Resolver+ -> Domain+ -> IO (Either DNSError [Domain])+lookupNSImpl lookup_function rlv dom = do+ erds <- lookup_function rlv dom NS+ case erds of+ -- See lookupXviaMX for an explanation of this construct.+ Left err -> return (Left err)+ Right rds -> return $ mapM unTag rds where- lookup' dps = check . catMaybes <$> mapM (func . fst) dps- check as = case as of- [] -> Nothing- ass -> Just (concat ass)+ unTag :: RData -> Either DNSError Domain+ unTag (RD_NS dm) = Right dm+ unTag _ = Left UnexpectedRDATA +-- | Look up all \'NS\' records for the given hostname. The results+-- are taken from the ANSWER section of the response (as opposed to+-- AUTHORITY). For details, see e.g.+-- <http://www.zytrax.com/books/dns/ch15/>.+--+-- There will typically be more than one name server for a+-- domain. It is therefore extra important to sort the results if+-- you prefer them to be at all deterministic.+--+-- Examples:+--+-- >>> import Data.List (sort)+-- >>> rs <- makeResolvSeed defaultResolvConf+-- >>> ns <- withResolver rs $ \resolver -> lookupNS resolver "mew.org"+-- >>> fmap sort ns+-- Right ["ns1.mew.org.","ns2.mew.org."]+--+lookupNS :: Resolver -> Domain -> IO (Either DNSError [Domain])+lookupNS = lookupNSImpl DNS.lookup++-- | Look up all \'NS\' records for the given hostname. The results+-- are taken from the AUTHORITY section of the response and not the+-- usual ANSWER (use 'lookupNS' for that). For details, see e.g.+-- <http://www.zytrax.com/books/dns/ch15/>.+--+-- There will typically be more than one name server for a+-- domain. It is therefore extra important to sort the results if+-- you prefer them to be at all deterministic.+--+-- For an example, we can look up the nameservers for+-- \"example.com\" from one of the root servers, a.gtld-servers.net,+-- the IP address of which was found beforehand:+--+-- >>> import Data.List (sort)+-- >>> let ri = RCHostName "192.5.6.30" -- a.gtld-servers.net+-- >>> let rc = defaultResolvConf { resolvInfo = ri }+-- >>> rs <- makeResolvSeed rc+-- >>> ns <- withResolver rs $ \resolver -> lookupNSAuth resolver "example.com"+-- >>> fmap sort ns+-- Right ["a.iana-servers.net.","b.iana-servers.net."]+--+lookupNSAuth :: Resolver -> Domain -> IO (Either DNSError [Domain])+lookupNSAuth = lookupNSImpl DNS.lookupAuth++ ---------------------------------------------------------------- -{-|- Resolving 'Domain' by 'NS'.--}-lookupNS :: Resolver -> Domain -> IO (Maybe [Domain])-lookupNS rlv dom = toNS <$> DNS.lookup rlv dom NS+-- | Look up all \'TXT\' records for the given hostname. The results+-- are free-form 'ByteString's.+--+-- Two common uses for \'TXT\' records are+-- <http://en.wikipedia.org/wiki/Sender_Policy_Framework> and+-- <http://en.wikipedia.org/wiki/DomainKeys_Identified_Mail>. As an+-- example, we find the SPF record for \"mew.org\":+--+-- >>> rs <- makeResolvSeed defaultResolvConf+-- >>> withResolver rs $ \resolver -> lookupTXT resolver "mew.org"+-- Right ["v=spf1 +mx -all"]+--+lookupTXT :: Resolver -> Domain -> IO (Either DNSError [ByteString])+lookupTXT rlv dom = do+ erds <- DNS.lookup rlv dom TXT+ case erds of+ -- See lookupXviaMX for an explanation of this construct.+ Left err -> return (Left err)+ Right rds -> return $ mapM unTag rds where- toNS = fmap (map unTag)- unTag (RD_NS dm) = dm- unTag _ = error "lookupNS"+ unTag :: RData -> Either DNSError ByteString+ unTag (RD_TXT x) = Right x+ unTag _ = Left UnexpectedRDATA ---------------------------------------------------------------- -{-|- Resolving 'String' by 'TXT'.--}-lookupTXT :: Resolver -> Domain -> IO (Maybe [ByteString])-lookupTXT rlv dom = toTXT <$> DNS.lookup rlv dom TXT+-- | Look up the \'SOA\' record for the given domain. The result 7-tuple+-- consists of the \'mname\', \'rname\', \'serial\', \'refresh\', \'retry\',+-- \'expire\' and \'minimum\' fields of the SOA record.+--+-- An \@ separator is used between the first and second labels of the+-- \'rname\' field. Since \'rname\' is an email address, it often contains+-- periods within its first label. Presently, the trailing period is not+-- removed from the domain part of the \'rname\', but this may change in the+-- future. Users should be prepared to remove any trailing period before+-- using the \'rname\` as a contact email address.+--+-- >>> rs <- makeResolvSeed defaultResolvConf+-- >>> soa <- withResolver rs $ \resolver -> lookupSOA resolver "mew.org"+-- >>> map (\ (mn, rn, _, _, _, _, _) -> (mn, rn)) <$> soa+-- Right [("ns1.mew.org.","kazu@mew.org.")]+--+lookupSOA :: Resolver -> Domain -> IO (Either DNSError [(Domain,Mailbox,Word32,Word32,Word32,Word32,Word32)])+lookupSOA rlv dom = do+ erds <- DNS.lookup rlv dom SOA+ case erds of+ -- See lookupXviaMX for an explanation of this construct.+ Left err -> return (Left err)+ Right rds -> return $ mapM unTag rds where- toTXT = fmap (map unTag)- unTag (RD_TXT x) = x- unTag _ = error "lookupTXT"+ unTag :: RData -> Either DNSError (Domain,Mailbox,Word32,Word32,Word32,Word32,Word32)+ unTag (RD_SOA mn mr serial refresh retry expire mini) = Right (mn, mr, serial, refresh, retry, expire, mini)+ unTag _ = Left UnexpectedRDATA ---------------------------------------------------------------- -{-|- Resolving 'Domain' and its preference by 'PTR'.--}-lookupPTR :: Resolver -> Domain -> IO (Maybe [Domain])-lookupPTR rlv dom = toPTR <$> DNS.lookup rlv dom PTR+-- | Look up all \'PTR\' records for the given hostname. To perform a+-- reverse lookup on an IP address, you must first reverse its+-- octets and then append the suffix \".in-addr.arpa.\"+--+-- We look up the PTR associated with the IP address+-- 210.130.137.80, i.e., 80.137.130.210.in-addr.arpa:+--+-- >>> rs <- makeResolvSeed defaultResolvConf+-- >>> withResolver rs $ \resolver -> lookupPTR resolver "180.2.232.202.in-addr.arpa"+-- Right ["www.iij.ad.jp."]+--+-- The 'lookupRDNS' function is more suited to this particular task.+--+lookupPTR :: Resolver -> Domain -> IO (Either DNSError [Domain])+lookupPTR rlv dom = do+ erds <- DNS.lookup rlv dom PTR+ case erds of+ -- See lookupXviaMX for an explanation of this construct.+ Left err -> return (Left err)+ Right rds -> return $ mapM unTag rds where- toPTR = fmap (map unTag)- unTag (RD_PTR dm) = dm- unTag _ = error "lookupPTR"+ unTag :: RData -> Either DNSError Domain+ unTag (RD_PTR dm) = Right dm+ unTag _ = Left UnexpectedRDATA ++-- | Convenient wrapper around 'lookupPTR' to perform a reverse lookup+-- on a single IP address.+--+-- We repeat the example from 'lookupPTR', except now we pass the IP+-- address directly:+--+-- >>> rs <- makeResolvSeed defaultResolvConf+-- >>> withResolver rs $ \resolver -> lookupRDNS resolver "202.232.2.180"+-- Right ["www.iij.ad.jp."]+--+lookupRDNS :: Resolver -> Domain -> IO (Either DNSError [Domain])+lookupRDNS rlv ip = lookupPTR rlv dom+ where+ -- ByteString constants.+ dot = BS.pack "."+ suffix = BS.pack ".in-addr.arpa"++ octets = BS.split '.' ip+ reverse_ip = BS.intercalate dot (reverse octets)+ dom = reverse_ip `BS.append` suffix+ ---------------------------------------------------------------- -{-|- Resolving 'Domain' and its preference by 'SRV'.--}-lookupSRV :: Resolver -> Domain -> IO (Maybe [(Int,Int,Int,Domain)])-lookupSRV rlv dom = toSRV <$> DNS.lookup rlv dom SRV+-- | Look up all \'SRV\' records for the given hostname. SRV records+-- consist (see <https://tools.ietf.org/html/rfc2782>) of the+-- following four fields:+--+-- * Priority (lower is more-preferred)+--+-- * Weight (relative frequency with which to use this record+-- amongst all results with the same priority)+--+-- * Port (the port on which the service is offered)+--+-- * Target (the hostname on which the service is offered)+--+-- The first three are integral, and the target is another DNS+-- hostname. We therefore return a four-tuple+-- @(Int,Int,Int,'Domain')@.+--+-- Examples:+--+-- >>> rs <- makeResolvSeed defaultResolvConf+-- >>> withResolver rs $ \resolver -> lookupSRV resolver "_xmpp-server._tcp.jabber.ietf.org"+-- Right [(5,0,5269,"_dc-srv.6661af51975d._xmpp-server._tcp.jabber.ietf.org.")]++-- Though the "jabber.ietf.orgs" SRV record may prove reasonably stable, as+-- with anything else published in DNS it is subject to change. Also, this+-- example only works when connected to the Internet. Perhaps the above+-- example should be displayed in a format that is not recognized as a test+-- by "doctest".++lookupSRV :: Resolver -> Domain -> IO (Either DNSError [(Word16, Word16, Word16, Domain)])+lookupSRV rlv dom = do+ erds <- DNS.lookup rlv dom SRV+ case erds of+ -- See lookupXviaMX for an explanation of this construct.+ Left err -> return (Left err)+ Right rds -> return $ mapM unTag rds where- toSRV = fmap (map unTag)- unTag (RD_SRV pri wei prt dm) = (pri,wei,prt,dm)- unTag _ = error "lookupSRV"+ unTag :: RData -> Either DNSError (Word16, Word16, Word16, Domain)+ unTag (RD_SRV pri wei prt dm) = Right (pri,wei,prt,dm)+ unTag _ = Left UnexpectedRDATA
+ Network/DNS/LookupRaw.hs view
@@ -0,0 +1,298 @@+{-# LANGUAGE RecordWildCards #-}++module Network.DNS.LookupRaw (+ -- * Lookups returning requested RData+ lookup+ , lookupAuth+ -- * Lookups returning DNS Messages+ , lookupRaw+ , lookupRawCtl+ , lookupRawCtlRecv+ -- * DNS Message procesing+ , fromDNSMessage+ ) where++import Data.Hourglass (timeAdd, Seconds)+import Prelude hiding (lookup)+import Time.System (timeCurrent)+import Network.Socket (Socket)++import Network.DNS.IO+import Network.DNS.Imports hiding (lookup)+import Network.DNS.Memo+import Network.DNS.Transport+import Network.DNS.Types.Internal+import Network.DNS.Types.Resolver++-- $setup+-- >>> import Network.DNS.Resolver++----------------------------------------------------------------++-- | Look up resource records of a specified type for a domain,+-- collecting the results+-- from the ANSWER section of the response.+-- See the documentation of 'lookupRaw'+-- to understand the concrete behavior.+-- Cache is used if 'resolvCache' is 'Just'.+--+-- Example:+--+-- >>> rs <- makeResolvSeed defaultResolvConf+-- >>> withResolver rs $ \resolver -> lookup resolver "www.example.com" A+-- Right [93.184.216.34]+--+lookup :: Resolver -> Domain -> TYPE -> IO (Either DNSError [RData])+lookup = lookupSection Answer++-- | Look up resource records of a specified type for a domain,+-- collecting the results+-- from the AUTHORITY section of the response.+-- See the documentation of 'lookupRaw'+-- to understand the concrete behavior.+-- Cache is used even if 'resolvCache' is 'Just'.+lookupAuth :: Resolver -> Domain -> TYPE -> IO (Either DNSError [RData])+lookupAuth = lookupSection Authority++----------------------------------------------------------------++-- | Looking up resource records of a domain. The first parameter is one of+-- the field accessors of the 'DNSMessage' type -- this allows you to+-- choose which section (answer, authority, or additional) you would like+-- to inspect for the result.++lookupSection :: Section+ -> Resolver+ -> Domain+ -> TYPE+ -> IO (Either DNSError [RData])+lookupSection section rlv dom typ+ | section == Authority = lookupFreshSection rlv dom typ section+ | otherwise = case mcacheConf of+ Nothing -> lookupFreshSection rlv dom typ section+ Just cacheconf -> lookupCacheSection rlv dom typ cacheconf+ where+ mcacheConf = resolvCache $ resolvconf $ resolvseed rlv++lookupFreshSection :: Resolver+ -> Domain+ -> TYPE+ -> Section+ -> IO (Either DNSError [RData])+lookupFreshSection rlv dom typ section = do+ eans <- lookupRaw rlv dom typ+ case eans of+ Left err -> return $ Left err+ Right ans -> return $ fromDNSMessage ans toRData+ where+ correct ResourceRecord{..} = rrtype == typ+ toRData = map rdata . filter correct . sectionF+ sectionF = case section of+ Answer -> answer+ Authority -> authority++lookupCacheSection :: Resolver+ -> Domain+ -> TYPE+ -> CacheConf+ -> IO (Either DNSError [RData])+lookupCacheSection rlv dom typ cconf = do+ mx <- lookupCache (dom,typ) c+ case mx of+ Nothing -> do+ eans <- lookupRaw rlv dom typ+ case eans of+ Left err ->+ -- Probably a network error happens.+ -- We do not cache anything.+ return $ Left err+ Right ans -> do+ let ex = fromDNSMessage ans toRR+ case ex of+ Left NameError -> do+ let v = Left NameError+ cacheNegative cconf c key v ans+ return v+ Left e -> return $ Left e+ Right [] -> do+ let v = Right []+ cacheNegative cconf c key v ans+ return v+ Right rss -> do+ cachePositive cconf c key rss+ return $ Right $ map rdata rss+ Just (_,x) -> return x+ where+ toRR = filter (typ `isTypeOf`) . answer+ Just c = cache rlv+ key = (dom,typ)++cachePositive :: CacheConf -> Cache -> Key -> [ResourceRecord] -> IO ()+cachePositive cconf c key rss+ | ttl == 0 = return () -- does not cache anything+ | otherwise = insertPositive cconf c key (Right rds) ttl+ where+ rds = map rdata rss+ ttl = minimum $ map rrttl rss -- rss is non-empty++insertPositive :: CacheConf -> Cache -> Key -> Entry -> TTL -> IO ()+insertPositive CacheConf{..} c k v ttl = when (ttl /= 0) $ do+ ctime <- timeCurrent+ let tim = ctime `timeAdd` life+ insertCache k tim v c+ where+ life :: Seconds+ life = fromIntegral (maximumTTL `min` ttl)++cacheNegative :: CacheConf -> Cache -> Key -> Entry -> DNSMessage -> IO ()+cacheNegative cconf c key v ans = case soas of+ [] -> return () -- does not cache anything+ soa:_ -> insertNegative cconf c key v $ rrttl soa+ where+ soas = filter (SOA `isTypeOf`) $ authority ans++insertNegative :: CacheConf -> Cache -> Key -> Entry -> TTL -> IO ()+insertNegative _ c k v ttl = when (ttl /= 0) $ do+ ctime <- timeCurrent+ let tim = ctime `timeAdd` life+ insertCache k tim v c+ where+ life :: Seconds+ life = fromIntegral ttl++isTypeOf :: TYPE -> ResourceRecord -> Bool+isTypeOf t ResourceRecord{..} = rrtype == t++----------------------------------------------------------------++-- | Look up a name and return the entire DNS Response.+--+-- For a given DNS server, the queries are done:+--+-- * A new UDP socket bound to a new local port is created and+-- a new identifier is created atomically from the cryptographically+-- secure pseudo random number generator for the target DNS server.+-- Then UDP queries are tried with the limitation of 'resolvRetry'+-- (use EDNS if specifiecd).+-- If it appears that the target DNS server does not support EDNS,+-- it falls back to traditional queries.+--+-- * If the response is truncated, a new TCP socket bound to a new+-- local port is created. Then exactly one TCP query is retried.+--+--+-- If multiple DNS servers are specified 'ResolvConf' ('RCHostNames ')+-- or found ('RCFilePath'), either sequential lookup or+-- concurrent lookup is carried out:+--+-- * In sequential lookup ('resolvConcurrent' is False),+-- the query procedure above is processed+-- in the order of the DNS servers sequentially until a successful+-- response is received.+--+-- * In concurrent lookup ('resolvConcurrent' is True),+-- the query procedure above is processed+-- for each DNS server concurrently.+-- The first received response is accepted even if+-- it is an error.+--+-- Cache is not used even if 'resolvCache' is 'Just'.+--+--+-- The example code:+--+-- @+-- rs <- makeResolvSeed defaultResolvConf+-- withResolver rs $ \\resolver -> lookupRaw resolver \"www.example.com\" A+-- @+--+-- And the (formatted) expected output:+--+-- @+-- Right (DNSMessage+-- { header = DNSHeader+-- { identifier = 1,+-- flags = DNSFlags+-- { qOrR = QR_Response,+-- opcode = OP_STD,+-- authAnswer = False,+-- trunCation = False,+-- recDesired = True,+-- recAvailable = True,+-- rcode = NoErr,+-- authenData = False+-- },+-- },+-- question = [Question { qname = \"www.example.com.\",+-- qtype = A}],+-- answer = [ResourceRecord {rrname = \"www.example.com.\",+-- rrtype = A,+-- rrttl = 800,+-- rdlen = 4,+-- rdata = 93.184.216.119}],+-- authority = [],+-- additional = []})+-- @+--+-- AXFR requests cannot be performed with this interface.+--+-- >>> rs <- makeResolvSeed defaultResolvConf+-- >>> withResolver rs $ \resolver -> lookupRaw resolver "mew.org" AXFR+-- Left InvalidAXFRLookup+--+lookupRaw :: Resolver -- ^ Resolver obtained via 'withResolver'+ -> Domain -- ^ Query domain+ -> TYPE -- ^ Query RRtype+ -> IO (Either DNSError DNSMessage)+lookupRaw rslv dom typ = lookupRawCtl rslv dom typ mempty++-- | Similar to 'lookupRaw', but the default values of the RD, AD, CD and DO+-- flag bits, as well as various EDNS features, can be adjusted via the+-- 'QueryControls' parameter.+--+lookupRawCtl :: Resolver -- ^ Resolver obtained via 'withResolver'+ -> Domain -- ^ Query domain+ -> TYPE -- ^ Query RRtype+ -> QueryControls -- ^ Query flag and EDNS overrides+ -> IO (Either DNSError DNSMessage)+lookupRawCtl rslv dom typ ctls = resolve rslv dom typ ctls receive++-- | Similar to 'lookupRawCtl', but the recv action can be replaced with+-- something other than `Network.DNS.IO.receive`.+-- For example, in an environment where frequent retrieval of the current time+-- is a performance issue, you can pass the time from outside instead of+-- having `Network.DNS.IO.receive` retrieve the current time.+lookupRawCtlRecv :: Resolver -- ^ Resolver obtained via 'withResolver'+ -> Domain -- ^ Query domain+ -> TYPE -- ^ Query RRtype+ -> QueryControls -- ^ Query flag and EDNS overrides+ -> (Socket -> IO DNSMessage) -- ^ Action to receive message from socket+ -> IO (Either DNSError DNSMessage)+lookupRawCtlRecv = resolve++----------------------------------------------------------------++-- | Messages with a non-error RCODE are passed to the supplied function+-- for processing. Other messages are translated to 'DNSError' instances.+--+-- Note that 'NameError' is not a lookup error. The lookup is successful,+-- bearing the sad news that the requested domain does not exist. 'NameError'+-- responses may return a meaningful AD bit, may contain useful data in the+-- authority section, and even initial CNAME records that lead to the+-- ultimately non-existent domain. Applications that wish to process the+-- content of 'NameError' (NXDomain) messages will need to implement their+-- own RCODE handling.+--+fromDNSMessage :: DNSMessage -> (DNSMessage -> a) -> Either DNSError a+fromDNSMessage ans conv = case errcode ans of+ NoErr -> Right $ conv ans+ FormatErr -> Left FormatError+ ServFail -> Left ServerFailure+ NameErr -> Left NameError+ NotImpl -> Left NotImplemented+ Refused -> Left OperationRefused+ BadVers -> Left BadOptRecord+ BadRCODE -> Left $ DecodeError "Malformed EDNS message"+ _ -> Left UnknownDNSError+ where+ errcode = rcode . flags . header
Network/DNS/Resolver.hs view
@@ -1,195 +1,108 @@-{-# LANGUAGE CPP #-}-{-|- DNS Resolver and lookup functions.-- Sample code:--@- import qualified Network.DNS as DNS (lookup)- import Network.DNS hiding (lookup)- main :: IO ()- main = do- rs <- makeResolvSeed defaultResolvConf- withResolver rs $ \\resolver -> do- DNS.lookup resolver \"www.example.com\" A >>= print-@--}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE BangPatterns #-} +-- | Resolver related data types. module Network.DNS.Resolver (- -- * Documentation- -- ** Configuration for resolver- FileOrNumericHost(..), ResolvConf(..), defaultResolvConf- -- ** Intermediate data type for resolver- , ResolvSeed, makeResolvSeed- -- ** Type and function for resolver- , Resolver(..), withResolver- -- ** Looking up functions- , lookup, lookupRaw+ -- * Configuration for resolver+ ResolvConf+ , defaultResolvConf+ -- ** Accessors+ , resolvInfo+ , resolvTimeout+ , resolvRetry+ , resolvConcurrent+ , resolvCache+ , resolvQueryControls+ -- ** Specifying DNS servers+ , FileOrNumericHost(..)+ -- ** Configuring cache+ , CacheConf+ , defaultCacheConf+ , maximumTTL+ , pruningDelay+ -- * Intermediate data type for resolver+ , ResolvSeed+ , makeResolvSeed+ -- * Type and function for resolver+ , Resolver+ , withResolver+ , withResolvers ) where -import Control.Applicative-import Control.Exception-import Data.Char-import Data.Int-import Data.List hiding (find, lookup)-import Network.BSD-import Network.DNS.Decode-import Network.DNS.Encode-import Network.DNS.Internal-import Network.Socket hiding (send, sendTo, recv, recvFrom)-import Network.Socket.ByteString.Lazy-import Prelude hiding (lookup)-import System.Random-import System.Timeout--#if mingw32_HOST_OS == 1-import Network.Socket (send)-import qualified Data.ByteString.Lazy.Char8 as LB-import Control.Monad (when)-#endif-------------------------------------------------------------------{-|- Union type for 'FilePath' and 'HostName'. Specify 'FilePath' to- \"resolv.conf\" or numeric IP address in 'String' form.--}-data FileOrNumericHost = RCFilePath FilePath | RCHostName HostName--{-|- Type for resolver configuration--}-data ResolvConf = ResolvConf {- resolvInfo :: FileOrNumericHost- , resolvTimeout :: Int- -- | This field was obsoleted.- , resolvBufsize :: Integer-}--{-|- Default 'ResolvConf'.- 'resolvInfo' is 'RCFilePath' \"\/etc\/resolv.conf\".- 'resolvTimeout' is 3,000,000 micro seconds.- 'resolvBufsize' is 512. (obsoleted)--}-defaultResolvConf :: ResolvConf-defaultResolvConf = ResolvConf {- resolvInfo = RCFilePath "/etc/resolv.conf"- , resolvTimeout = 3 * 1000 * 1000- , resolvBufsize = 512-}--------------------------------------------------------------------{-|- Abstract data type of DNS Resolver seed--}-data ResolvSeed = ResolvSeed {- addrInfo :: AddrInfo- , rsTimeout :: Int- , rsBufsize :: Integer-}+import Control.Exception as E+import qualified Crypto.Random as C+import qualified Data.ByteString as BS+import Data.IORef (IORef)+import qualified Data.IORef as I+import qualified Data.List.NonEmpty as NE+import Network.Socket (AddrInfoFlag(..), AddrInfo(..), PortNumber, HostName, SocketType(Datagram), getAddrInfo, defaultHints)+import Prelude -{-|- Abstract data type of DNS Resolver--}-data Resolver = Resolver {- genId :: IO Int- , dnsSock :: Socket- , dnsTimeout :: Int- , dnsBufsize :: Integer-}+import Network.DNS.Imports+import Network.DNS.Memo+import Network.DNS.Resolver.Internal+import Network.DNS.Transport+import Network.DNS.Types.Internal+import Network.DNS.Types.Resolver ---------------------------------------------------------------- -{-|- Making 'ResolvSeed' from an IP address of a DNS cache server.--}+-- | Make a 'ResolvSeed' from a 'ResolvConf'.+--+-- Examples:+--+-- >>> rs <- makeResolvSeed defaultResolvConf+-- makeResolvSeed :: ResolvConf -> IO ResolvSeed-makeResolvSeed conf = ResolvSeed <$> addr- <*> pure (resolvTimeout conf)- <*> pure (resolvBufsize conf)+makeResolvSeed conf = ResolvSeed conf <$> findAddresses where- addr = case resolvInfo conf of- RCHostName numhost -> makeAddrInfo numhost- RCFilePath file -> toAddr <$> readFile file >>= makeAddrInfo- toAddr cs = let l:_ = filter ("nameserver" `isPrefixOf`) $ lines cs- in extract l- extract = reverse . dropWhile isSpace . reverse . dropWhile isSpace . drop 11+ findAddresses :: IO (NonEmpty AddrInfo)+ findAddresses = case resolvInfo conf of+ RCHostName numhost -> (:| []) <$> makeAddrInfo numhost Nothing+ RCHostPort numhost mport -> (:| []) <$> makeAddrInfo numhost (Just mport)+ RCHostNames nss -> mkAddrs nss+ RCFilePath file -> getDefaultDnsServers file >>= mkAddrs+ mkAddrs [] = E.throwIO BadConfiguration+ mkAddrs (l:ls) = (:|) <$> makeAddrInfo l Nothing <*> forM ls (`makeAddrInfo` Nothing) -makeAddrInfo :: HostName -> IO AddrInfo-makeAddrInfo addr = do- proto <- getProtocolNumber "udp"+makeAddrInfo :: HostName -> Maybe PortNumber -> IO AddrInfo+makeAddrInfo addr mport = do let hints = defaultHints {- addrFlags = [AI_ADDRCONFIG, AI_NUMERICHOST, AI_PASSIVE]+ addrFlags = [AI_ADDRCONFIG, AI_NUMERICHOST, AI_NUMERICSERV, AI_PASSIVE] , addrSocketType = Datagram- , addrProtocol = proto }- a:_ <- getAddrInfo (Just hints) (Just addr) (Just "domain")- return a+ -- 53 is the standard port number for domain name servers as assigned by IANA+ serv = maybe "53" show mport+ head <$> getAddrInfo (Just hints) (Just addr) (Just serv) ---------------------------------------------------------------- -{-|- Giving a thread-safe 'Resolver' to the function of the second- argument. 'withResolver' should be passed to 'forkIO'.--}-+-- | Giving a thread-safe 'Resolver' to the function of the second+-- argument. withResolver :: ResolvSeed -> (Resolver -> IO a) -> IO a-withResolver seed func = do- let ai = addrInfo seed- sock <- socket (addrFamily ai) (addrSocketType ai) (addrProtocol ai)- connect sock (addrAddress ai)- let resolv = Resolver {- genId = getRandom- , dnsSock = sock- , dnsTimeout = rsTimeout seed- , dnsBufsize = rsBufsize seed- }- func resolv `finally` sClose sock--getRandom :: IO Int-getRandom = getStdRandom (randomR (0,65535))------------------------------------------------------------------+withResolver seed f = makeResolver seed >>= f -{-|- Looking up resource records of a domain.--}-lookup :: Resolver -> Domain -> TYPE -> IO (Maybe [RDATA])-lookup rlv dom typ = (>>= toRDATA) <$> lookupRaw rlv dom typ- where- {- CNAME hack- dom' = if "." `isSuffixOf` dom then dom else dom ++ "."- correct r = rrname r == dom' && rrtype r == typ- -}- correct r = rrtype r == typ- listToMaybe [] = Nothing- listToMaybe xs = Just xs- toRDATA = listToMaybe . map rdata . filter correct . answer+{-# DEPRECATED withResolvers "Use withResolver with resolvConcurrent set to True" #-}+-- | Giving thread-safe 'Resolver's to the function of the second+-- argument. For each 'Resolver', multiple lookups must be done+-- sequentially. 'Resolver's can be used concurrently.+withResolvers :: [ResolvSeed] -> ([Resolver] -> IO a) -> IO a+withResolvers seeds f = mapM makeResolver seeds >>= f -{-|- Looking up a domain and returning an entire DNS Response.--}-lookupRaw :: Resolver -> Domain -> TYPE -> IO (Maybe DNSFormat)-lookupRaw rlv dom typ = do- seqno <- genId rlv- sendAll sock (composeQuery seqno [q])- (>>= check seqno) <$> timeout tm (receive sock)- where- sock = dnsSock rlv- tm = dnsTimeout rlv- q = makeQuestion dom typ- check seqno res = do- let hdr = header res- if identifier hdr == seqno then- Just res- else- Nothing+makeResolver :: ResolvSeed -> IO Resolver+makeResolver seed = do+ let n = NE.length $ nameservers seed+ refs <- replicateM n (C.drgNew >>= I.newIORef)+ let gens = NE.fromList $ map getRandom refs+ case resolvCache $ resolvconf seed of+ Just cacheconf -> do+ c <- newCache $ pruningDelay cacheconf+ return $ Resolver seed gens $ Just c+ Nothing -> return $ Resolver seed gens Nothing -#if mingw32_HOST_OS == 1- -- Windows does not support sendAll in Network.ByteString.Lazy.- -- This implements sendAll with Haskell Strings.- sendAll sock bs = do- sent <- send sock (LB.unpack bs)- when (sent < fromIntegral (LB.length bs)) $ sendAll sock (LB.drop (fromIntegral sent) bs)-#endif+getRandom :: IORef C.ChaChaDRG -> IO Word16+getRandom ref = I.atomicModifyIORef' ref $ \gen ->+ let (bs, gen') = C.randomBytesGenerate 2 gen+ [u,l] = map fromIntegral $ BS.unpack bs+ !seqno = u * 256 + l+ in (gen', seqno)
− Network/DNS/StateBinary.hs
@@ -1,169 +0,0 @@-{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}-module Network.DNS.StateBinary where--import Blaze.ByteString.Builder-import Control.Applicative-import Control.Monad.State-import Data.Attoparsec.ByteString-import qualified Data.Attoparsec.ByteString.Lazy as AL-import Data.ByteString (ByteString)-import qualified Data.ByteString as BS (unpack, length)-import qualified Data.ByteString.Lazy as BL (ByteString)-import Data.Conduit-import Data.Conduit.Attoparsec-import Data.Int-import Data.IntMap (IntMap)-import qualified Data.IntMap as IM (insert, lookup, empty)-import Data.Map (Map)-import qualified Data.Map as M (insert, lookup, empty)-import Data.Monoid-import Data.Word-import Network.DNS.Types-import Prelude hiding (lookup, take)--import qualified Data.Attoparsec.Types as T (Parser)--------------------------------------------------------------------type SPut = State WState Write--data WState = WState {- wsDomain :: Map Domain Int- , wsPosition :: Int-}--initialWState :: WState-initialWState = WState M.empty 0--instance Monoid SPut where- mempty = return mempty- mappend a b = mconcat <$> sequence [a, b]--put8 :: Word8 -> SPut-put8 = fixedSized 1 writeWord8--put16 :: Word16 -> SPut-put16 = fixedSized 2 writeWord16be--put32 :: Word32 -> SPut-put32 = fixedSized 4 writeWord32be--putInt8 :: Int -> SPut-putInt8 = fixedSized 1 (writeInt8 . fromIntegral)--putInt16 :: Int -> SPut-putInt16 = fixedSized 2 (writeInt16be . fromIntegral)--putInt32 :: Int -> SPut-putInt32 = fixedSized 4 (writeInt32be . fromIntegral)--putByteString :: ByteString -> SPut-putByteString = writeSized BS.length writeByteString--addPositionW :: Int -> State WState ()-addPositionW n = do- (WState m cur) <- get- put $ WState m (cur+n)--fixedSized :: Int -> (a -> Write) -> a -> SPut-fixedSized n f a = do addPositionW n- return (f a)--writeSized :: Show a => (a -> Int) -> (a -> Write) -> a -> SPut-writeSized n f a = do addPositionW (n a)- return (f a)--wsPop :: Domain -> State WState (Maybe Int)-wsPop dom = do- doms <- gets wsDomain- return $ M.lookup dom doms--wsPush :: Domain -> Int -> State WState ()-wsPush dom pos = do- (WState m cur) <- get- put $ WState (M.insert dom pos m) cur--------------------------------------------------------------------type SGet = StateT PState (T.Parser ByteString)--data PState = PState {- psDomain :: IntMap Domain- , psPosition :: Int- }--------------------------------------------------------------------getPosition :: SGet Int-getPosition = psPosition <$> get--addPosition :: Int -> SGet ()-addPosition n = do- PState dom pos <- get- put $ PState dom (pos + n)--push :: Int -> Domain -> SGet ()-push n d = do- PState dom pos <- get- put $ PState (IM.insert n d dom) pos--pop :: Int -> SGet (Maybe Domain)-pop n = IM.lookup n . psDomain <$> get--------------------------------------------------------------------get8 :: SGet Word8-get8 = lift anyWord8 <* addPosition 1--get16 :: SGet Word16-get16 = lift getWord16be <* addPosition 2- where- word8' = fromIntegral <$> anyWord8- getWord16be = do- a <- word8'- b <- word8'- return $ a * 256 + b--get32 :: SGet Word32-get32 = lift getWord32be <* addPosition 4- where- word8' = fromIntegral <$> anyWord8- getWord32be = do- a <- word8'- b <- word8'- c <- word8'- d <- word8'- return $ a * 1677721 + b * 65536 + c * 256 + d--getInt8 :: SGet Int-getInt8 = fromIntegral <$> get8--getInt16 :: SGet Int-getInt16 = fromIntegral <$> get16--getInt32 :: SGet Int-getInt32 = fromIntegral <$> get32--------------------------------------------------------------------getNBytes :: Int -> SGet [Int]-getNBytes len = toInts <$> getNByteString len- where- toInts = map fromIntegral . BS.unpack--getNByteString :: Int -> SGet ByteString-getNByteString n = lift (take n) <* addPosition n--------------------------------------------------------------------initialState :: PState-initialState = PState IM.empty 0--sinkSGet :: SGet a -> Sink ByteString (ResourceT IO) (a, PState)-sinkSGet parser = sinkParser (runStateT parser initialState)--runSGet :: SGet a -> BL.ByteString -> Either String (a, PState)-runSGet parser bs = AL.eitherResult $ AL.parse (runStateT parser initialState) bs--runSPut :: SPut -> BL.ByteString-runSPut = toLazyByteString . fromWrite . flip evalState initialWState
+ Network/DNS/Transport.hs view
@@ -0,0 +1,245 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveDataTypeable #-}++module Network.DNS.Transport (+ Resolver(..)+ , resolve+ ) where++import Control.Concurrent.Async (async, waitAnyCancel)+import Control.Exception as E+import qualified Data.ByteString.Char8 as BS+import qualified Data.List.NonEmpty as NE+import Network.Socket (AddrInfo(..), SockAddr(..), Family(AF_INET, AF_INET6), Socket, SocketType(Stream), close, socket, connect, defaultProtocol)+import System.IO.Error (annotateIOError)+import System.Timeout (timeout)++import Network.DNS.IO+import Network.DNS.Imports+import Network.DNS.Types.Internal+import Network.DNS.Types.Resolver++-- | Check response for a matching identifier and question. If we ever do+-- pipelined TCP, we'll need to handle out of order responses. See:+-- https://tools.ietf.org/html/rfc7766#section-7+--+checkResp :: Question -> Identifier -> DNSMessage -> Bool+checkResp q seqno = isNothing . checkRespM q seqno++-- When the response 'RCODE' is 'FormatErr', the server did not understand our+-- query packet, and so is not expected to return a matching question.+--+checkRespM :: Question -> Identifier -> DNSMessage -> Maybe DNSError+checkRespM q seqno resp+ | identifier (header resp) /= seqno = Just SequenceNumberMismatch+ | FormatErr <- rcode $ flags $ header resp+ , [] <- question resp = Nothing+ | [q] /= question resp = Just QuestionMismatch+ | otherwise = Nothing++----------------------------------------------------------------++data TCPFallback = TCPFallback deriving (Show, Typeable)+instance Exception TCPFallback++type Rslv0 = QueryControls -> (Socket -> IO DNSMessage)+ -> IO (Either DNSError DNSMessage)++type Rslv1 = Question+ -> Int -- Timeout+ -> Int -- Retry+ -> Rslv0++type TcpRslv = AddrInfo+ -> Question+ -> Int -- Timeout+ -> QueryControls+ -> IO DNSMessage++type UdpRslv = Int -- Retry+ -> (Socket -> IO DNSMessage)+ -> TcpRslv++-- In lookup loop, we try UDP until we get a response. If the response+-- is truncated, we try TCP once, with no further UDP retries.+--+-- For now, we optimize for low latency high-availability caches+-- (e.g. running on a loopback interface), where TCP is cheap+-- enough. We could attempt to complete the TCP lookup within the+-- original time budget of the truncated UDP query, by wrapping both+-- within a a single 'timeout' thereby staying within the original+-- time budget, but it seems saner to give TCP a full opportunity to+-- return results. TCP latency after a truncated UDP reply will be+-- atypical.+--+-- Future improvements might also include support for TCP on the+-- initial query.+--+-- This function merges the query flag overrides from the resolver+-- configuration with any additional overrides from the caller.+--+resolve :: Resolver -> Domain -> TYPE -> Rslv0+resolve rlv dom typ qctls rcv+ | isIllegal dom = return $ Left IllegalDomain+ | typ == AXFR = return $ Left InvalidAXFRLookup+ | onlyOne = resolveOne (head nss) (head gens) q tm retry ctls rcv+ | concurrent = resolveConcurrent nss gens q tm retry ctls rcv+ | otherwise = resolveSequential nss gens q tm retry ctls rcv+ where+ q = case BS.last dom of+ '.' -> Question dom typ+ _ -> Question (dom <> ".") typ++ gens = NE.toList $ genIds rlv++ seed = resolvseed rlv+ nss = NE.toList $ nameservers seed+ onlyOne = length nss == 1+ ctls = qctls <> resolvQueryControls (resolvconf $ resolvseed rlv)++ conf = resolvconf seed+ concurrent = resolvConcurrent conf+ tm = resolvTimeout conf+ retry = resolvRetry conf+++resolveSequential :: [AddrInfo] -> [IO Identifier] -> Rslv1+resolveSequential nss gs q tm retry ctls rcv = loop nss gs+ where+ loop [ai] [gen] = resolveOne ai gen q tm retry ctls rcv+ loop (ai:ais) (gen:gens) = do+ eres <- resolveOne ai gen q tm retry ctls rcv+ case eres of+ Left _ -> loop ais gens+ res -> return res+ loop _ _ = error "resolveSequential:loop"++resolveConcurrent :: [AddrInfo] -> [IO Identifier] -> Rslv1+resolveConcurrent nss gens q tm retry ctls rcv = do+ asyncs <- mapM mkAsync $ zip nss gens+ snd <$> waitAnyCancel asyncs+ where+ mkAsync (ai,gen) = async $ resolveOne ai gen q tm retry ctls rcv++resolveOne :: AddrInfo -> IO Identifier -> Rslv1+resolveOne ai gen q tm retry ctls rcv =+ E.try $ udpTcpLookup gen retry rcv ai q tm ctls++----------------------------------------------------------------++-- UDP attempts must use the same ID and accept delayed answers+-- but we use a fresh ID for each TCP lookup.+--+udpTcpLookup :: IO Identifier -> UdpRslv+udpTcpLookup gen retry rcv ai q tm ctls = do+ ident <- gen+ udpLookup ident retry rcv ai q tm ctls `E.catch`+ \TCPFallback -> tcpLookup gen ai q tm ctls++----------------------------------------------------------------++ioErrorToDNSError :: AddrInfo -> String -> IOError -> IO DNSMessage+ioErrorToDNSError ai protoName ioe = throwIO $ NetworkFailure aioe+ where+ loc = protoName ++ "@" ++ show (addrAddress ai)+ aioe = annotateIOError ioe loc Nothing Nothing++----------------------------------------------------------------++udpOpen :: AddrInfo -> IO Socket+udpOpen ai = do+ sock <- socket (addrFamily ai) (addrSocketType ai) (addrProtocol ai)+ connect sock (addrAddress ai)+ return sock++-- This throws DNSError or TCPFallback.+udpLookup :: Identifier -> UdpRslv+udpLookup ident retry rcv ai q tm ctls = do+ let qry = encodeQuestion ident q ctls+ E.handle (ioErrorToDNSError ai "udp") $+ bracket (udpOpen ai) close (loop qry ctls 0 RetryLimitExceeded)+ where+ loop qry lctls cnt err sock+ | cnt == retry = E.throwIO err+ | otherwise = do+ mres <- timeout tm (send sock qry >> getAns sock)+ case mres of+ Nothing -> loop qry lctls (cnt + 1) RetryLimitExceeded sock+ Just res -> do+ let fl = flags $ header res+ tc = trunCation fl+ rc = rcode fl+ eh = ednsHeader res+ cs = ednsEnabled FlagClear <> lctls+ if tc then E.throwIO TCPFallback+ else if rc == FormatErr && eh == NoEDNS && cs /= lctls+ then let qry' = encodeQuestion ident q cs+ in loop qry' cs cnt RetryLimitExceeded sock+ else return res++ -- | Closed UDP ports are occasionally re-used for a new query, with+ -- the nameserver returning an unexpected answer to the wrong socket.+ -- Such answers should be simply dropped, with the client continuing+ -- to wait for the right answer, without resending the question.+ -- Note, this eliminates sequence mismatch as a UDP error condition,+ -- instead we'll time out if no matching answer arrives.+ --+ getAns sock = do+ resp <- rcv sock+ if checkResp q ident resp+ then return resp+ else getAns sock++----------------------------------------------------------------++-- Create a TCP socket with the given socket address.+tcpOpen :: SockAddr -> IO Socket+tcpOpen peer = case peer of+ SockAddrInet{} -> socket AF_INET Stream defaultProtocol+ SockAddrInet6{} -> socket AF_INET6 Stream defaultProtocol+ _ -> E.throwIO ServerFailure++-- Perform a DNS query over TCP, if we were successful in creating+-- the TCP socket.+-- This throws DNSError only.+tcpLookup :: IO Identifier -> TcpRslv+tcpLookup gen ai q tm ctls =+ E.handle (ioErrorToDNSError ai "tcp") $ do+ res <- bracket (tcpOpen addr) close (perform ctls)+ let rc = rcode $ flags $ header res+ eh = ednsHeader res+ cs = ednsEnabled FlagClear <> ctls+ -- If we first tried with EDNS, retry without on FormatErr.+ if rc == FormatErr && eh == NoEDNS && cs /= ctls+ then bracket (tcpOpen addr) close (perform cs)+ else return res+ where+ addr = addrAddress ai+ perform cs vc = do+ ident <- gen+ let qry = encodeQuestion ident q cs+ mres <- timeout tm $ do+ connect vc addr+ sendVC vc qry+ receiveVC vc+ case mres of+ Nothing -> E.throwIO TimeoutExpired+ Just res -> maybe (return res) E.throwIO (checkRespM q ident res)++----------------------------------------------------------------++badLength :: Domain -> Bool+badLength dom+ | BS.null dom = True+ | BS.last dom == '.' = BS.length dom > 254+ | otherwise = BS.length dom > 253++isIllegal :: Domain -> Bool+isIllegal dom+ | badLength dom = True+ | '.' `BS.notElem` dom = True+ | ':' `BS.elem` dom = True+ | '/' `BS.elem` dom = True+ | any (\x -> BS.length x > 63)+ (BS.split '.' dom) = True+ | otherwise = False
Network/DNS/Types.hs view
@@ -1,26 +1,134 @@-{-|- Data types for DNS Query and Response. For more information, see <http://www.ietf.org/rfc/rfc1035>.--}+-- | Data types for DNS Query and Response.+-- For more information, see <http://www.ietf.org/rfc/rfc1035>. module Network.DNS.Types (- -- * Domain- Domain- -- * TYPE- , TYPE (..), intToType, typeToInt, toType- -- * DNS Format- , DNSFormat (..)- -- * DNS Header+ -- * Resource Records+ ResourceRecord (..)+ , Answers+ , AuthorityRecords+ , AdditionalRecords+ -- ** Types+ , Domain+ , CLASS+ , classIN+ , TTL+ -- ** Resource Record Types+ , TYPE (+ A+ , NS+ , CNAME+ , SOA+ , NULL+ , PTR+ , MX+ , TXT+ , AAAA+ , SRV+ , DNAME+ , OPT+ , DS+ , RRSIG+ , NSEC+ , DNSKEY+ , NSEC3+ , NSEC3PARAM+ , TLSA+ , CDS+ , CDNSKEY+ , CSYNC+ , AXFR+ , ANY+ , CAA+ )+ , fromTYPE+ , toTYPE+ -- ** Resource Data+ , RData (..)+ , RD_RRSIG(..)+ , dnsTime+ -- * DNS Message+ , DNSMessage (..)+ -- ** Query+ , makeQuery+ , makeEmptyQuery+ , defaultQuery+ -- ** Query Controls+ , QueryControls+ , rdFlag+ , adFlag+ , cdFlag+ , doFlag+ , ednsEnabled+ , ednsSetVersion+ , ednsSetUdpSize+ , ednsSetOptions+ -- *** Flag and OData control operations+ , FlagOp(..)+ , ODataOp(..)+ -- ** Response+ , defaultResponse+ , makeResponse+ -- ** DNS Header , DNSHeader (..)- -- * DNS Flags+ , Identifier+ -- *** DNS flags , DNSFlags (..)- -- * DNS Body , QorR (..)+ , defaultDNSFlags+ -- *** OPCODE and RCODE , OPCODE (..)- , RCODE (..)- , ResourceRecord (..)+ , fromOPCODE+ , toOPCODE+ , RCODE (+ NoErr+ , FormatErr+ , ServFail+ , NameErr+ , NotImpl+ , Refused+ , YXDomain+ , YXRRSet+ , NXRRSet+ , NotAuth+ , NotZone+ , BadVers+ , BadKey+ , BadTime+ , BadMode+ , BadName+ , BadAlg+ , BadTrunc+ , BadCookie+ , BadRCODE+ )+ , fromRCODE+ , toRCODE+ -- ** EDNS Pseudo-Header+ , EDNSheader(..)+ , ifEDNS+ , mapEDNS+ -- *** EDNS record+ , EDNS(..)+ , defaultEDNS+ , maxUdpSize+ , minUdpSize+ -- *** EDNS options+ , OData (..)+ , OptCode (+ ClientSubnet+ , DAU+ , DHU+ , N3U+ , NSID+ )+ , fromOptCode+ , toOptCode+ -- ** DNS Body , Question (..)- , RDATA (..)- , responseA, responseAAAA- ) where+ -- * DNS Error+ , DNSError (..)+ -- * Other types+ , Mailbox+ ) where -import Network.DNS.Internal+import Network.DNS.Types.Internal
+ Network/DNS/Utils.hs view
@@ -0,0 +1,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
Setup.hs view
@@ -1,2 +1,4 @@ import Distribution.Simple++main :: IO () main = defaultMain
+ cabal.project view
@@ -0,0 +1,3 @@+packages: .+optimization: True+write-ghc-environment-files: always
+ cbits/dns.c view
@@ -0,0 +1,77 @@+#include <winsock2.h>+#include <iphlpapi.h>+#include <string.h>+#include <windows.h>+#include <string.h>++#define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x))+#define FREE(x) HeapFree(GetProcessHeap(), 0, (x))++// Fills `dnsAddresses` with the DNS addresses found, up to `bufferLen`.+// Returns NO_ERROR (0x0) in case the operation succeeds, otherwise a non-zero+// error code. See: https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx+DWORD getWindowsDefDnsServers(char* dnsAddresses, size_t bufferLen) {+ FIXED_INFO *pFixedInfo;+ ULONG ulOutBufLen;+ DWORD dwRetVal;++ if (bufferLen <= 0) return ERROR_NOT_ENOUGH_MEMORY;++ pFixedInfo = (FIXED_INFO *) MALLOC(sizeof (FIXED_INFO));+ if (pFixedInfo == NULL)+ return ERROR_NOT_ENOUGH_MEMORY;+ ulOutBufLen = sizeof (FIXED_INFO);++ // Make an initial call to GetAdaptersInfo to get the necessary size into the+ // ulOutBufLen variable+ if (GetNetworkParams(pFixedInfo, &ulOutBufLen) == ERROR_BUFFER_OVERFLOW) {+ FREE(pFixedInfo);+ pFixedInfo = (FIXED_INFO *) MALLOC(ulOutBufLen);+ if (pFixedInfo == NULL)+ return ERROR_NOT_ENOUGH_MEMORY;+ }++ dwRetVal = GetNetworkParams(pFixedInfo, &ulOutBufLen);++ if (dwRetVal == NO_ERROR) {+ int offset = 0;+ int spaceAvailable = bufferLen;+ IP_ADDR_STRING* head = &pFixedInfo->DnsServerList;+ int count = 0;++ while (head != NULL) {+ int ipLen = strlen(head->IpAddress.String);+ int copySize = ipLen + 1;++ spaceAvailable -= copySize;+ if (spaceAvailable >= 0) {+ // Write the separator.+ // The string is already terminated due to the call to+ // strcpy_s, which copies the null terminator.+ if (count != 0) dnsAddresses[offset - 1] = ',';+ // Copy the IP address, including the null terminator.+ strcpy_s(dnsAddresses + offset, copySize, head->IpAddress.String);+ if (spaceAvailable == 0) break;+ } else+ break;++ offset += copySize;+ count++;+ head = head->Next;+ }++ }++ if (pFixedInfo) FREE(pFixedInfo);+ return dwRetVal;+}++/*++// Test with 'gcc -o dnsServer -Wall -Werror -pedantic -liphlpapi -Iinclude dns.c' on a+// Windows machine.++int main(){+ printf(getWindowsDefDnsServers()->dnsAddresses);+ return 0;+}*/
dns.cabal view
@@ -1,91 +1,138 @@-Name: dns-Version: 0.3.8-Author: Kazu Yamamoto <kazu@iij.ad.jp>-Maintainer: Kazu Yamamoto <kazu@iij.ad.jp>-License: BSD3-License-File: LICENSE-Synopsis: DNS library in Haskell-Description: DNS library for clients and servers.-Category: Network-Cabal-Version: >= 1.10-Build-Type: Simple+cabal-version: 2.0+name: dns+version: 4.2.0+license: BSD3+license-file: LICENSE+maintainer: Kazu Yamamoto <kazu@iij.ad.jp>+author: Kazu Yamamoto <kazu@iij.ad.jp>+tested-with:+ ghc ==8.0.2 ghc ==8.2.2 ghc ==8.4.4 ghc ==8.6.5 ghc ==8.8.1 -Library- Default-Language: Haskell2010- GHC-Options: -Wall- Exposed-Modules: Network.DNS- Network.DNS.Lookup- Network.DNS.Resolver- Network.DNS.Types- Network.DNS.Encode- Network.DNS.Decode- Other-Modules: Network.DNS.Internal- Network.DNS.StateBinary- if impl(ghc >= 7)- Build-Depends: base >= 4 && < 5- , attoparsec- , attoparsec-conduit- , binary- , blaze-builder- , bytestring- , conduit >= 0.5- , containers- , iproute >= 1.2.4- , mtl- , network >= 2.3- , network-conduit- , random- else- Build-Depends: base >= 4 && < 5- , attoparsec- , attoparsec-conduit- , binary- , blaze-builder- , bytestring- , conduit- , containers- , iproute >= 1.2.4- , mtl- , network- , network-bytestring- , network-conduit- , random+synopsis: DNS library in Haskell+description:+ A thread-safe DNS library for both clients and servers written+ in pure Haskell. -Test-Suite network- Type: exitcode-stdio-1.0- Default-Language: Haskell2010- Hs-Source-Dirs: test2- Ghc-Options: -Wall- Main-Is: Spec.hs- Other-Modules: LookupSpec- Build-Depends: dns- , base- , bytestring- , hspec+category: Network+build-type: Simple+extra-source-files:+ Changelog.md+ cabal.project+ cbits/dns.c -Test-Suite spec- Type: exitcode-stdio-1.0- Default-Language: Haskell2010- Hs-Source-Dirs: test, .- Ghc-Options: -Wall- Main-Is: Spec.hs- Other-Modules: EncodeSpec- Build-Depends: base- , attoparsec- , attoparsec-conduit- , binary- , blaze-builder- , bytestring- , conduit >= 0.5- , containers- , dns- , hspec- , iproute >= 1.2.4- , mtl- , network >= 2.3- , network-conduit- , random+source-repository head+ type: git+ location: https://github.com/kazu-yamamoto/dns.git -Source-Repository head- Type: git- Location: git://github.com/kazu-yamamoto/dns.git+library+ exposed-modules:+ Network.DNS+ Network.DNS.Lookup+ Network.DNS.LookupRaw+ Network.DNS.Resolver+ Network.DNS.Utils+ Network.DNS.Types+ Network.DNS.Decode+ Network.DNS.Encode+ Network.DNS.IO++ other-modules: Network.DNS.Transport+ default-language: Haskell2010+ ghc-options: -Wall+ build-depends:+ dns-internal,+ base >=4 && <5,+ array,+ async,+ attoparsec,+ auto-update,+ base16-bytestring,+ base64-bytestring,+ bytestring,+ containers,+ crypton,+ hourglass,+ iproute >=1.3.2,+ mtl,+ network >=2.3,+ psqueues++library dns-internal+ exposed-modules:+ Network.DNS.Imports+ Network.DNS.Types.Internal+ Network.DNS.Types.Resolver+ Network.DNS.Resolver.Internal+ Network.DNS.Decode.Parsers+ Network.DNS.Decode.Internal+ Network.DNS.Encode.Builders+ Network.DNS.Encode.Internal+ Network.DNS.StateBinary+ Network.DNS.Memo+ Network.DNS.Base32Hex++ hs-source-dirs: internal+ default-language: Haskell2010+ ghc-options: -Wall+ build-depends:+ base,+ array,+ async,+ attoparsec,+ auto-update,+ base16-bytestring,+ base64-bytestring,+ bytestring,+ case-insensitive,+ containers,+ crypton,+ hourglass,+ iproute,+ mtl,+ network,+ psqueues++ if os(windows)+ c-sources: cbits/dns.c+ extra-libraries: iphlpapi++test-suite network-tests+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ build-tool-depends: hspec-discover:hspec-discover+ hs-source-dirs: test2+ other-modules:+ LookupSpec+ IOSpec++ default-language: Haskell2010+ ghc-options: -Wall+ build-depends:+ dns,+ dns-internal,+ base,+ hspec,+ network++test-suite spec-tests+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ build-tool-depends: hspec-discover:hspec-discover+ hs-source-dirs: test+ other-modules:+ EncodeSpec+ DecodeSpec+ RoundTripSpec++ default-language: Haskell2010+ ghc-options: -Wall+ build-depends:+ dns,+ dns-internal,+ QuickCheck >=2.9,+ base,+ bytestring,+ case-insensitive,+ hspec,+ iproute >=1.3.2,+ word8
+ internal/Network/DNS/Base32Hex.hs view
@@ -0,0 +1,50 @@+module Network.DNS.Base32Hex (encode) where++import qualified Data.Array.MArray as A+import qualified Data.Array.IArray as A+import qualified Data.Array.ST as A+import qualified Data.ByteString as B++import Network.DNS.Imports++-- | Encode ByteString using the+-- <https://tools.ietf.org/html/rfc4648#section-7 RFC4648 base32hex>+-- encoding with no padding as specified for the+-- <https://tools.ietf.org/html/rfc5155#section-3.3 RFC5155 Next Hashed Owner Name>+-- field.+--+encode :: B.ByteString -- ^ input buffer+ -> B.ByteString -- ^ base32hex output+encode bs =+ let len = (8 * B.length bs + 4) `div` 5+ ws = B.unpack bs+ in B.pack $ A.elems $ A.runSTUArray $ do+ a <- A.newArray (0 :: Int, len-1) 0+ go ws a 0+ where+ toHex32 w | w < 10 = 48 + w+ | otherwise = 55 + w++ load8 a i = A.readArray a i+ store8 a i v = A.writeArray a i v++ -- Encode a list of 8-bit words at bit offset @n@+ -- into an array 'a' of 5-bit words.+ go [] a _ = A.mapArray toHex32 a+ go (w:ws) a n = do+ -- Split 8 bits into left, middle and right parts. The+ -- right part only gets written when the 8-bit input word+ -- splits across three different 5-bit words.+ --+ let (q, r) = n `divMod` 5+ wl = w `shiftR` ( 3 + r)+ wm = (w `shiftL` ( 5 - r)) `shiftR` 3+ wr = (w `shiftL` (10 - r)) `shiftR` 3+ al <- case r of+ 0 -> pure wl+ _ -> (wl .|.) <$> load8 a q+ store8 a q al+ store8 a (q + 1) wm+ when (r > 2) $ store8 a (q+2) wr+ go ws a $ n + 8+{-# INLINE encode #-}
+ internal/Network/DNS/Decode/Internal.hs view
@@ -0,0 +1,64 @@+{-# OPTIONS_HADDOCK hide #-}++module Network.DNS.Decode.Internal (+ -- ** Internal message component decoders for tests+ decodeDNSHeader+ , decodeDNSFlags+ , decodeDomain+ , decodeMailbox+ , decodeResourceRecordAt+ , decodeResourceRecord+ ) where++import Network.DNS.Decode.Parsers+import Network.DNS.Imports+import Network.DNS.StateBinary+import Network.DNS.Types.Internal++----------------------------------------------------------------++-- | Decode the 'DNSFlags' field of 'DNSHeader'. This is an internal function+-- exposed only for testing.+--+decodeDNSFlags :: ByteString -> Either DNSError DNSFlags+decodeDNSFlags bs = fst <$> runSGet getDNSFlags bs++-- | Decode the 'DNSHeader' of a message. This is an internal function.+-- exposed only for testing.+--+decodeDNSHeader :: ByteString -> Either DNSError DNSHeader+decodeDNSHeader bs = fst <$> runSGet getHeader bs++-- | Decode a domain name. Since DNS names may use name compression, it is not+-- generally possible to decode the names separately from the enclosing DNS+-- message. This is an internal function exposed only for testing.+--+decodeDomain :: ByteString -> Either DNSError Domain+decodeDomain bs = fst <$> runSGet getDomain bs++-- | Decode a mailbox name (e.g. the SOA record /rname/ field). Since DNS names+-- may use name compression, it is not generally possible to decode the names+-- separately from the enclosing DNS message. This is an internal function.+--+decodeMailbox :: ByteString -> Either DNSError Mailbox+decodeMailbox bs = fst <$> runSGet getMailbox bs++-- | Decoding resource records.++-- | Decode a resource record (RR) with any DNS timestamps interpreted at the+-- nominal epoch time (see 'decodeAt'). Since RRs may use name compression,+-- it is not generally possible to decode resource record separately from the+-- enclosing DNS message. This is an internal function.+--+decodeResourceRecord :: ByteString -> Either DNSError ResourceRecord+decodeResourceRecord bs = fst <$> runSGet getResourceRecord bs++-- | Decode a resource record (RR) with DNS timestamps interpreted at the+-- supplied epoch time. Since RRs may use DNS name compression, it is not+-- generally possible to decode resource record separately from the enclosing+-- DNS message. This is an internal function.+--+decodeResourceRecordAt :: Int64 -- ^ current epoch time+ -> ByteString -- ^ encoded resource record+ -> Either DNSError ResourceRecord+decodeResourceRecordAt t bs = fst <$> runSGetAt t getResourceRecord bs
+ internal/Network/DNS/Decode/Parsers.hs view
@@ -0,0 +1,508 @@+{-# LANGUAGE BangPatterns, LambdaCase, OverloadedStrings #-}++module Network.DNS.Decode.Parsers (+ getResponse+ , getDNSFlags+ , getHeader+ , getResourceRecord+ , getResourceRecords+ , getDomain+ , getMailbox+ ) where++import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BS+import qualified Data.CaseInsensitive as CI+import qualified Data.IP+import Data.IP (IP(..), toIPv4, toIPv6b, makeAddrRange)++import Network.DNS.Imports+import Network.DNS.StateBinary+import Network.DNS.Types.Internal++----------------------------------------------------------------++getResponse :: SGet DNSMessage+getResponse = do+ hm <- getHeader+ qdCount <- getInt16+ anCount <- getInt16+ nsCount <- getInt16+ arCount <- getInt16+ queries <- getQueries qdCount+ answers <- getResourceRecords anCount+ authrrs <- getResourceRecords nsCount+ addnrrs <- getResourceRecords arCount+ let (opts, rest) = partition ((==) OPT. rrtype) addnrrs+ flgs = flags hm+ rc = fromRCODE $ rcode flgs+ (eh, erc) = getEDNS rc opts+ hd = hm { flags = flgs { rcode = erc } }+ pure $ DNSMessage hd eh queries answers authrrs $ ifEDNS eh rest addnrrs++ where++ -- | Get EDNS pseudo-header and the high eight bits of the extended RCODE.+ --+ getEDNS :: Word16 -> AdditionalRecords -> (EDNSheader, RCODE)+ getEDNS rc rrs = case rrs of+ [rr] | Just (edns, erc) <- optEDNS rr+ -> (EDNSheader edns, toRCODE erc)+ [] -> (NoEDNS, toRCODE rc)+ _ -> (InvalidEDNS, BadRCODE)++ where++ -- | Extract EDNS information from an OPT RR.+ --+ optEDNS :: ResourceRecord -> Maybe (EDNS, Word16)+ optEDNS (ResourceRecord "." OPT udpsiz ttl' (RD_OPT opts)) =+ let hrc = fromIntegral rc .&. 0x0f+ erc = shiftR (ttl' .&. 0xff000000) 20 .|. hrc+ secok = ttl' `testBit` 15+ vers = fromIntegral $ shiftR (ttl' .&. 0x00ff0000) 16+ in Just (EDNS vers udpsiz secok opts, fromIntegral erc)+ optEDNS _ = Nothing++----------------------------------------------------------------++getDNSFlags :: SGet DNSFlags+getDNSFlags = do+ flgs <- get16+ oc <- getOpcode flgs+ return $ DNSFlags (getQorR flgs)+ oc+ (getAuthAnswer flgs)+ (getTrunCation flgs)+ (getRecDesired flgs)+ (getRecAvailable flgs)+ (getRcode flgs)+ (getAuthenData flgs)+ (getChkDisable flgs)+ where+ getQorR w = if testBit w 15 then QR_Response else QR_Query+ getOpcode w =+ case shiftR w 11 .&. 0x0f of+ n | Just opc <- toOPCODE n+ -> pure opc+ | otherwise+ -> failSGet $ "Unsupported header opcode: " ++ show n+ getAuthAnswer w = testBit w 10+ getTrunCation w = testBit w 9+ getRecDesired w = testBit w 8+ getRecAvailable w = testBit w 7+ getRcode w = toRCODE $ w .&. 0x0f+ getAuthenData w = testBit w 5+ getChkDisable w = testBit w 4++----------------------------------------------------------------++getHeader :: SGet DNSHeader+getHeader =+ DNSHeader <$> decodeIdentifier <*> getDNSFlags+ where+ decodeIdentifier = get16++----------------------------------------------------------------++getQueries :: Int -> SGet [Question]+getQueries n = replicateM n getQuery++getTYPE :: SGet TYPE+getTYPE = toTYPE <$> get16++-- XXX: Include the class when implemented, or otherwise perhaps check the+-- implicit assumption that the class is classIN.+--+getQuery :: SGet Question+getQuery = Question <$> getDomain+ <*> getTYPE+ <* ignoreClass+ where+ ignoreClass = get16++getResourceRecords :: Int -> SGet [ResourceRecord]+getResourceRecords n = replicateM n getResourceRecord++getResourceRecord :: SGet ResourceRecord+getResourceRecord = do+ dom <- getDomain+ typ <- getTYPE+ cls <- get16+ ttl <- get32+ len <- getInt16+ dat <- fitSGet len $ getRData typ len+ return $ ResourceRecord dom typ cls ttl dat++----------------------------------------------------------------++-- | Helper to find position of RData end, that is, the offset of the first+-- byte /after/ the current RData.+--+rdataEnd :: Int -- ^ number of bytes left from current position+ -> SGet Int -- ^ end position+rdataEnd !len = (+) len <$> getPosition++getRData :: TYPE -> Int -> SGet RData+getRData NS _ = RD_NS <$> getDomain+getRData MX _ = RD_MX <$> get16 <*> getDomain+getRData CNAME _ = RD_CNAME <$> getDomain+getRData DNAME _ = RD_DNAME <$> getDomain+getRData TXT len = RD_TXT <$> getTXT len+getRData A _ = RD_A . toIPv4 <$> getNBytes 4+getRData AAAA _ = RD_AAAA . toIPv6b <$> getNBytes 16+getRData SOA _ = RD_SOA <$> getDomain+ <*> getMailbox+ <*> decodeSerial+ <*> decodeRefesh+ <*> decodeRetry+ <*> decodeExpire+ <*> decodeMinimum+ where+ decodeSerial = get32+ decodeRefesh = get32+ decodeRetry = get32+ decodeExpire = get32+ decodeMinimum = get32+getRData PTR _ = RD_PTR <$> getDomain+getRData SRV _ = RD_SRV <$> decodePriority+ <*> decodeWeight+ <*> decodePort+ <*> getDomain+ where+ decodePriority = get16+ decodeWeight = get16+ decodePort = get16+--+getRData RP _ = RD_RP <$> getMailbox+ <*> getDomain+--+getRData OPT len = RD_OPT <$> getOpts len+--+getRData TLSA len = RD_TLSA <$> decodeUsage+ <*> decodeSelector+ <*> decodeMType+ <*> decodeADF+ where+ decodeUsage = get8+ decodeSelector = get8+ decodeMType = get8+ decodeADF = getNByteString (len - 3)+--+getRData DS len = RD_DS <$> decodeTag+ <*> decodeAlg+ <*> decodeDtyp+ <*> decodeDval+ where+ decodeTag = get16+ decodeAlg = get8+ decodeDtyp = get8+ decodeDval = getNByteString (len - 4)+--+getRData CDS len = RD_CDS <$> decodeTag+ <*> decodeAlg+ <*> decodeDtyp+ <*> decodeDval+ where+ decodeTag = get16+ decodeAlg = get8+ decodeDtyp = get8+ decodeDval = getNByteString (len - 4)+--+getRData RRSIG len = RD_RRSIG <$> decodeRRSIG+ where+ decodeRRSIG = do+ -- The signature follows a variable length zone name+ -- and occupies the rest of the RData. Simplest to+ -- checkpoint the position at the start of the RData,+ -- and after reading the zone name, and subtract that+ -- from the RData length.+ --+ end <- rdataEnd len+ typ <- getTYPE+ alg <- get8+ cnt <- get8+ ttl <- get32+ tex <- getDnsTime+ tin <- getDnsTime+ tag <- get16+ dom <- getDomain -- XXX: Enforce no compression?+ pos <- getPosition+ val <- getNByteString $ end - pos+ return $ RDREP_RRSIG typ alg cnt ttl tex tin tag dom val+ getDnsTime = do+ tnow <- getAtTime+ tdns <- get32+ return $! dnsTime tdns tnow+--+getRData NULL len = RD_NULL <$> getNByteString len+getRData NSEC len = do+ end <- rdataEnd len+ dom <- getDomain+ pos <- getPosition+ RD_NSEC dom <$> getNsecTypes (end - pos)+--+getRData DNSKEY len = RD_DNSKEY <$> decodeKeyFlags+ <*> decodeKeyProto+ <*> decodeKeyAlg+ <*> decodeKeyBytes+ where+ decodeKeyFlags = get16+ decodeKeyProto = get8+ decodeKeyAlg = get8+ decodeKeyBytes = getNByteString (len - 4)+--+getRData CDNSKEY len = RD_CDNSKEY <$> decodeKeyFlags+ <*> decodeKeyProto+ <*> decodeKeyAlg+ <*> decodeKeyBytes+ where+ decodeKeyFlags = get16+ decodeKeyProto = get8+ decodeKeyAlg = get8+ decodeKeyBytes = getNByteString (len - 4)+--+getRData NSEC3 len = do+ dend <- rdataEnd len+ halg <- get8+ flgs <- get8+ iter <- get16+ salt <- getInt8 >>= getNByteString+ hash <- getInt8 >>= getNByteString+ tpos <- getPosition+ RD_NSEC3 halg flgs iter salt hash <$> getNsecTypes (dend - tpos)+--+getRData NSEC3PARAM _ = RD_NSEC3PARAM <$> decodeHashAlg+ <*> decodeFlags+ <*> decodeIterations+ <*> decodeSalt+ where+ decodeHashAlg = get8+ decodeFlags = get8+ decodeIterations = get16+ decodeSalt = getInt8 >>= getNByteString+--+getRData CAA len = do+ dend <- rdataEnd len+ flags <- get8+ tag <- getInt8 >>= getNByteString+ tpos <- getPosition+ RD_CAA flags (CI.mk tag) <$> getNByteString (dend - tpos)+--+getRData _ len = UnknownRData <$> getNByteString len++----------------------------------------------------------------++-- $+--+-- >>> import Network.DNS.StateBinary+-- >>> let Right ((t,_),l) = runSGetWithLeftovers (getTXT 8) "\3foo\3barbaz"+-- >>> (t, l) == ("foobar", "baz")+-- True++-- | Concatenate a sequence of length-prefixed strings of text+-- https://tools.ietf.org/html/rfc1035#section-3.3+--+getTXT :: Int -> SGet ByteString+getTXT !len = B.concat <$> sGetMany "TXT RR string" len getstring+ where+ getstring = getInt8 >>= getNByteString++-- <https://tools.ietf.org/html/rfc6891#section-6.1.2>+-- Parse a list of EDNS options+--+getOpts :: Int -> SGet [OData]+getOpts !len = sGetMany "EDNS option" len getoption+ where+ getoption = do+ code <- toOptCode <$> get16+ olen <- getInt16+ getOData code olen++-- <https://tools.ietf.org/html/rfc4034#section-4.1>+-- Parse a list of NSEC type bitmaps+--+getNsecTypes :: Int -> SGet [TYPE]+getNsecTypes !len = concat <$> sGetMany "NSEC type bitmap" len getbits+ where+ getbits = do+ window <- flip shiftL 8 <$> getInt8+ blocks <- getInt8+ when (blocks > 32) $+ failSGet $ "NSEC bitmap block too long: " ++ show blocks+ concatMap blkTypes. zip [window, window + 8..] <$> getNBytes blocks+ where+ blkTypes (bitOffset, byte) =+ [ toTYPE $ fromIntegral $ bitOffset + i |+ i <- [0..7], byte .&. bit (7-i) /= 0 ]++----------------------------------------------------------------++getOData :: OptCode -> Int -> SGet OData+getOData NSID len = OD_NSID <$> getNByteString len+getOData DAU len = OD_DAU <$> getNoctets len+getOData DHU len = OD_DHU <$> getNoctets len+getOData N3U len = OD_N3U <$> getNoctets len+getOData ClientSubnet len = do+ family <- get16+ srcBits <- get8+ scpBits <- get8+ addrbs <- getNByteString (len - 4) -- 4 = 2 + 1 + 1+ --+ -- https://tools.ietf.org/html/rfc7871#section-6+ --+ -- o ADDRESS, variable number of octets, contains either an IPv4 or+ -- IPv6 address, depending on FAMILY, which MUST be truncated to the+ -- number of bits indicated by the SOURCE PREFIX-LENGTH field,+ -- padding with 0 bits to pad to the end of the last octet needed.+ --+ -- o A server receiving an ECS option that uses either too few or too+ -- many ADDRESS octets, or that has non-zero ADDRESS bits set beyond+ -- SOURCE PREFIX-LENGTH, SHOULD return FORMERR to reject the packet,+ -- as a signal to the software developer making the request to fix+ -- their implementation.+ --+ -- In order to avoid needless decoding errors, when the ECS encoding+ -- requirements are violated, we construct an OD_ECSgeneric OData,+ -- instread of an IP-specific OD_ClientSubnet OData, which will only+ -- be used for valid inputs. When the family is neither IPv4(1) nor+ -- IPv6(2), or the address prefix is not correctly encoded (too long+ -- or too short), the OD_ECSgeneric data contains the verbatim input+ -- from the peer.+ --+ case BS.length addrbs == (fromIntegral srcBits + 7) `div` 8 of+ True | Just ip <- bstoip family addrbs srcBits scpBits+ -> pure $ OD_ClientSubnet srcBits scpBits ip+ _ -> pure $ OD_ECSgeneric family srcBits scpBits addrbs+ where+ prefix addr bits = Data.IP.addr $ makeAddrRange addr $ fromIntegral bits+ zeropad = (++ repeat 0). map fromIntegral. B.unpack+ checkBits fromBytes toIP srcBits scpBits bytes =+ let addr = fromBytes bytes+ maskedAddr = prefix addr srcBits+ maxBits = fromIntegral $ 8 * length bytes+ in if addr == maskedAddr && scpBits <= maxBits+ then Just $ toIP addr+ else Nothing+ bstoip :: Word16 -> B.ByteString -> Word8 -> Word8 -> Maybe IP+ bstoip family bs srcBits scpBits = case family of+ 1 -> checkBits toIPv4 IPv4 srcBits scpBits $ take 4 $ zeropad bs+ 2 -> checkBits toIPv6b IPv6 srcBits scpBits $ take 16 $ zeropad bs+ _ -> Nothing+getOData opc len = UnknownOData (fromOptCode opc) <$> getNByteString len++----------------------------------------------------------------++-- | Pointers MUST point back into the packet per RFC1035 Section 4.1.4. This+-- is further interpreted by the DNS community (from a discussion on the IETF+-- DNSOP mailing list) to mean that they don't point back into the same domain.+-- Therefore, when starting to parse a domain, the current offset is also a+-- strict upper bound on the targets of any pointers that arise while processing+-- the domain. When following a pointer, the target again becomes a stict upper+-- bound for any subsequent pointers. This results in a simple loop-prevention+-- algorithm, each sequence of valid pointer values is necessarily strictly+-- decreasing! The third argument to 'getDomain'' is a strict pointer upper+-- bound, and is set here to the position at the start of parsing the domain+-- or mailbox.+--+-- Note: the separator passed to 'getDomain'' is required to be either \'.\' or+-- \'\@\', or else 'unparseLabel' needs to be modified to handle the new value.+--++getDomain :: SGet Domain+getDomain = getPosition >>= getDomain' dot++getMailbox :: SGet Mailbox+getMailbox = getPosition >>= getDomain' atsign++dot, atsign :: Word8+dot = fromIntegral $ fromEnum '.' -- 46+atsign = fromIntegral $ fromEnum '@' -- 64++-- $+-- Pathological case: pointer embedded inside a label! The pointer points+-- behind the start of the domain and is then absorbed into the initial label!+-- Though we don't IMHO have to support this, it is not manifestly illegal, and+-- does exercise the code in an interesting way. Ugly as this is, it also+-- "works" the same in Perl's Net::DNS and reportedly in ISC's BIND.+--+-- >>> :{+-- let input = "\6\3foo\192\0\3bar\0"+-- parser = skipNBytes 1 >> getDomain' dot 1+-- Right (output, _) = runSGet parser input+-- in output == "foo.\\003foo\\192\\000.bar."+-- :}+-- True+--+-- The case below fails to point far enough back, and triggers the loop+-- prevention code-path.+--+-- >>> :{+-- let input = "\6\3foo\192\1\3bar\0"+-- parser = skipNBytes 1 >> getDomain' dot 1+-- Left (DecodeError err) = runSGet parser input+-- in err+-- :}+-- "invalid name compression pointer"++-- | Get a domain name, using sep1 as the separator between the 1st and 2nd+-- label. Subsequent labels (and always the trailing label) are terminated+-- with a ".".+--+-- Note: the separator is required to be either \'.\' or \'\@\', or else+-- 'unparseLabel' needs to be modified to handle the new value.+--+-- Domain name compression pointers must always refer to a position that+-- precedes the start of the current domain name. The starting offsets form a+-- strictly decreasing sequence, which prevents pointer loops.+--+getDomain' :: Word8 -> Int -> SGet ByteString+getDomain' sep1 ptrLimit = do+ pos <- getPosition+ c <- getInt8+ let n = getValue c+ getdomain pos c n+ where+ -- Reprocess the same ByteString starting at the pointer+ -- target (offset).+ getPtr pos offset = do+ msg <- getInput+ let parser = skipNBytes offset >> getDomain' sep1 offset+ case runSGet parser msg of+ Left (DecodeError err) -> failSGet err+ Left err -> fail $ show err+ Right o -> do+ -- Cache only the presentation form decoding of domain names,+ -- mailboxes (e.g. SOA rname) are less frequently reused, and+ -- have a different presentation form, so must not share the+ -- same cache.+ when (sep1 == dot) $+ push pos (fst o)+ return (fst o)++ getdomain pos c n+ | c == 0 = return "." -- Perhaps the root domain?+ | isPointer c = do+ d <- getInt8+ let offset = n * 256 + d+ when (offset >= ptrLimit) $+ failSGet "invalid name compression pointer"+ if sep1 /= dot+ then getPtr pos offset+ else pop offset >>= \case+ Nothing -> getPtr pos offset+ Just o -> return o+ -- As for now, extended labels have no use.+ -- This may change some time in the future.+ | isExtLabel c = return ""+ | otherwise = do+ hs <- unparseLabel sep1 <$> getNByteString n+ ds <- getDomain' dot ptrLimit+ let dom = case ds of -- avoid trailing ".."+ "." -> hs <> "."+ _ -> hs <> B.singleton sep1 <> ds+ push pos dom+ return dom+ getValue c = c .&. 0x3f+ isPointer c = testBit c 7 && testBit c 6+ isExtLabel c = not (testBit c 7) && testBit c 6
+ internal/Network/DNS/Encode/Builders.hs view
@@ -0,0 +1,361 @@+{-# LANGUAGE+ BangPatterns+ , RecordWildCards+ , TransformListComp+ #-}++-- | DNS Message builder.+module Network.DNS.Encode.Builders (+ putDNSMessage+ , putDNSFlags+ , putHeader+ , putDomain+ , putMailbox+ , putResourceRecord+ ) where++import Control.Monad.State (State, modify, execState, gets)+import qualified Control.Exception as E+import qualified Data.ByteString.Builder as BB+import qualified Data.ByteString.Char8 as BS+import qualified Data.ByteString.Lazy.Char8 as LBS+import qualified Data.CaseInsensitive as CI+import qualified Data.IP+import Data.IP (IP(..), fromIPv4, fromIPv6b, makeAddrRange)+import GHC.Exts (the, groupWith)++import Network.DNS.Imports+import Network.DNS.StateBinary+import Network.DNS.Types.Internal++----------------------------------------------------------------++putDNSMessage :: DNSMessage -> SPut+putDNSMessage msg = putHeader hd+ <> putNums+ <> mconcat (map putQuestion qs)+ <> mconcat (map putResourceRecord an)+ <> mconcat (map putResourceRecord au)+ <> mconcat (map putResourceRecord ad)+ where+ putNums = mconcat $ fmap putInt16 [ length qs+ , length an+ , length au+ , length ad+ ]+ hm = header msg+ fl = flags hm+ eh = ednsHeader msg+ qs = question msg+ an = answer msg+ au = authority msg+ hd = ifEDNS eh hm $ hm { flags = fl { rcode = rc } }+ rc = ifEDNS eh <$> id <*> nonEDNSrcode $ rcode fl+ where+ nonEDNSrcode code | fromRCODE code < 16 = code+ | otherwise = FormatErr+ ad = prependOpt $ additional msg+ where+ prependOpt ads = mapEDNS eh (fromEDNS ads $ fromRCODE rc) ads+ where+ fromEDNS :: AdditionalRecords -> Word16 -> EDNS -> AdditionalRecords+ fromEDNS rrs rc' edns = ResourceRecord name' type' class' ttl' rdata' : rrs+ where+ name' = BS.singleton '.'+ type' = OPT+ class' = maxUdpSize `min` (minUdpSize `max` ednsUdpSize edns)+ ttl0' = fromIntegral (rc' .&. 0xff0) `shiftL` 20+ vers' = fromIntegral (ednsVersion edns) `shiftL` 16+ ttl'+ | ednsDnssecOk edns = ttl0' `setBit` 15 .|. vers'+ | otherwise = ttl0' .|. vers'+ rdata' = RD_OPT $ ednsOptions edns++putHeader :: DNSHeader -> SPut+putHeader hdr = putIdentifier (identifier hdr)+ <> putDNSFlags (flags hdr)+ where+ putIdentifier = put16++putDNSFlags :: DNSFlags -> SPut+putDNSFlags DNSFlags{..} = put16 word+ where+ set :: Word16 -> State Word16 ()+ set byte = modify (.|. byte)++ st :: State Word16 ()+ st = sequence_+ [ set (fromRCODE rcode .&. 0x0f)+ , when chkDisable $ set (bit 4)+ , when authenData $ set (bit 5)+ , when recAvailable $ set (bit 7)+ , when recDesired $ set (bit 8)+ , when trunCation $ set (bit 9)+ , when authAnswer $ set (bit 10)+ , set (fromOPCODE opcode `shiftL` 11)+ , when (qOrR==QR_Response) $ set (bit 15)+ ]++ word = execState st 0++-- XXX: Use question class when implemented+--+putQuestion :: Question -> SPut+putQuestion Question{..} = putDomain qname+ <> put16 (fromTYPE qtype)+ <> put16 classIN++putResourceRecord :: ResourceRecord -> SPut+putResourceRecord ResourceRecord{..} = mconcat [+ putDomain rrname+ , put16 (fromTYPE rrtype)+ , put16 rrclass+ , put32 rrttl+ , putResourceRData rdata+ ]+ where+ putResourceRData :: RData -> SPut+ putResourceRData rd = do+ addPositionW 2 -- "simulate" putInt16+ rDataBuilder <- putRData rd+ let rdataLength = fromIntegral . LBS.length . BB.toLazyByteString $ rDataBuilder+ let rlenBuilder = BB.int16BE rdataLength+ return $ rlenBuilder <> rDataBuilder+++putRData :: RData -> SPut+putRData rd = case rd of+ RD_A address -> mconcat $ map putInt8 (fromIPv4 address)+ RD_NS nsdname -> putDomain nsdname+ RD_CNAME cname -> putDomain cname+ RD_SOA a b c d e f g -> putSOA a b c d e f g+ RD_NULL bytes -> putByteString bytes+ RD_PTR ptrdname -> putDomain ptrdname+ RD_MX pref exch -> mconcat [put16 pref, putDomain exch]+ RD_TXT textstring -> putTXT textstring+ RD_RP mbox dname -> putMailbox mbox <> putDomain dname+ RD_AAAA address -> mconcat $ map putInt8 (fromIPv6b address)+ RD_SRV pri wei prt tgt -> putSRV pri wei prt tgt+ RD_DNAME dname -> putDomain dname+ RD_OPT options -> mconcat $ fmap putOData options+ RD_DS kt ka dt d -> putDS kt ka dt d+ RD_CDS kt ka dt d -> putDS kt ka dt d+ RD_RRSIG rrsig -> putRRSIG rrsig+ RD_NSEC next types -> putDomain next <> putNsecTypes types+ RD_DNSKEY f p alg key -> putDNSKEY f p alg key+ RD_CDNSKEY f p alg key -> putDNSKEY f p alg key+ RD_NSEC3 a f i s h types -> putNSEC3 a f i s h types+ RD_NSEC3PARAM a f iter salt -> putNSEC3PARAM a f iter salt+ RD_TLSA u s m dgst -> putTLSA u s m dgst+ RD_CAA f t v -> putCAA f t v+ UnknownRData bytes -> putByteString bytes+ where+ putSOA mn mr serial refresh retry expire minttl = mconcat+ [ putDomain mn+ , putMailbox mr+ , put32 serial+ , put32 refresh+ , put32 retry+ , put32 expire+ , put32 minttl+ ]+ -- TXT record string fragments are at most 255 bytes+ putTXT textstring =+ let (!h, !t) = BS.splitAt 255 textstring+ in putByteStringWithLength h <> if BS.null t+ then mempty+ else putTXT t+ putSRV priority weight port target = mconcat+ [ put16 priority+ , put16 weight+ , put16 port+ , putDomain target+ ]+ putDS keytag keyalg digestType digest = mconcat+ [ put16 keytag+ , put8 keyalg+ , put8 digestType+ , putByteString digest+ ]+ putRRSIG RDREP_RRSIG{..} = mconcat+ [ put16 $ fromTYPE rrsigType+ , put8 rrsigKeyAlg+ , put8 rrsigNumLabels+ , put32 rrsigTTL+ , put32 $ fromIntegral rrsigExpiration+ , put32 $ fromIntegral rrsigInception+ , put16 rrsigKeyTag+ , putDomain rrsigZone+ , putByteString rrsigValue+ ]+ putDNSKEY flags protocol alg key = mconcat+ [ put16 flags+ , put8 protocol+ , put8 alg+ , putByteString key+ ]+ putNSEC3 alg flags iterations salt hash types = mconcat+ [ put8 alg+ , put8 flags+ , put16 iterations+ , putByteStringWithLength salt+ , putByteStringWithLength hash+ , putNsecTypes types+ ]+ putNSEC3PARAM alg flags iterations salt = mconcat+ [ put8 alg+ , put8 flags+ , put16 iterations+ , putByteStringWithLength salt+ ]+ putTLSA usage selector mtype assocData = mconcat+ [ put8 usage+ , put8 selector+ , put8 mtype+ , putByteString assocData+ ]+ putCAA flags tag value = mconcat+ [ put8 flags+ , putByteStringWithLength (CI.original tag)+ , putByteString value+ ]++-- | Encode DNSSEC NSEC type bits+putNsecTypes :: [TYPE] -> SPut+putNsecTypes types = putTypeList $ map fromTYPE types+ where+ putTypeList :: [Word16] -> SPut+ putTypeList ts =+ mconcat [ putWindow (the top8) bot8 |+ t <- ts,+ let top8 = fromIntegral t `shiftR` 8,+ let bot8 = fromIntegral t .&. 0xff,+ then group by top8+ using groupWith ]++ putWindow :: Int -> [Int] -> SPut+ putWindow top8 bot8s =+ let blks = maximum bot8s `shiftR` 3+ in putInt8 top8+ <> put8 (1 + fromIntegral blks)+ <> putBits 0 [ (the block, foldl' mergeBits 0 bot8) |+ bot8 <- bot8s,+ let block = bot8 `shiftR` 3,+ then group by block+ using groupWith ]+ where+ -- | Combine type bits in network bit order, i.e. bit 0 first.+ mergeBits acc b = setBit acc (7 - b.&.0x07)++ putBits :: Int -> [(Int, Word8)] -> SPut+ putBits _ [] = pure mempty+ putBits n ((block, octet) : rest) =+ putReplicate (block-n) 0+ <> put8 octet+ <> putBits (block + 1) rest++-- | Encode EDNS OPTION consisting of a list of octets.+putODWords :: Word16 -> [Word8] -> SPut+putODWords code ws =+ mconcat [ put16 code+ , putInt16 $ length ws+ , mconcat $ map put8 ws+ ]++-- | Encode an EDNS OPTION byte string.+putODBytes :: Word16 -> ByteString -> SPut+putODBytes code bs =+ mconcat [ put16 code+ , putInt16 $ BS.length bs+ , putByteString bs+ ]++putOData :: OData -> SPut+putOData (OD_NSID nsid) = putODBytes (fromOptCode NSID) nsid+putOData (OD_DAU as) = putODWords (fromOptCode DAU) as+putOData (OD_DHU hs) = putODWords (fromOptCode DHU) hs+putOData (OD_N3U hs) = putODWords (fromOptCode N3U) hs+putOData (OD_ClientSubnet srcBits scpBits ip) =+ -- https://tools.ietf.org/html/rfc7871#section-6+ --+ -- o ADDRESS, variable number of octets, contains either an IPv4 or+ -- IPv6 address, depending on FAMILY, which MUST be truncated to the+ -- number of bits indicated by the SOURCE PREFIX-LENGTH field,+ -- padding with 0 bits to pad to the end of the last octet needed.+ --+ -- o A server receiving an ECS option that uses either too few or too+ -- many ADDRESS octets, or that has non-zero ADDRESS bits set beyond+ -- SOURCE PREFIX-LENGTH, SHOULD return FORMERR to reject the packet,+ -- as a signal to the software developer making the request to fix+ -- their implementation.+ --+ let octets = fromIntegral $ (srcBits + 7) `div` 8+ prefix addr = Data.IP.addr $ makeAddrRange addr $ fromIntegral srcBits+ (family, raw) = case ip of+ IPv4 ip4 -> (1, take octets $ fromIPv4 $ prefix ip4)+ IPv6 ip6 -> (2, take octets $ fromIPv6b $ prefix ip6)+ dataLen = 2 + 2 + octets+ in mconcat [ put16 $ fromOptCode ClientSubnet+ , putInt16 dataLen+ , put16 family+ , put8 srcBits+ , put8 scpBits+ , mconcat $ fmap putInt8 raw+ ]+putOData (OD_ECSgeneric family srcBits scpBits addr) =+ mconcat [ put16 $ fromOptCode ClientSubnet+ , putInt16 $ 4 + BS.length addr+ , put16 family+ , put8 srcBits+ , put8 scpBits+ , putByteString addr+ ]+putOData (UnknownOData code bs) = putODBytes code bs++-- In the case of the TXT record, we need to put the string length+-- fixme : What happens with the length > 256 ?+putByteStringWithLength :: BS.ByteString -> SPut+putByteStringWithLength bs = putInt8 (fromIntegral $ BS.length bs) -- put the length of the given string+ <> putByteString bs++----------------------------------------------------------------++rootDomain :: Domain+rootDomain = BS.pack "."++putDomain :: Domain -> SPut+putDomain = putDomain' '.'++putMailbox :: Mailbox -> SPut+putMailbox = putDomain' '@'++putDomain' :: Char -> ByteString -> SPut+putDomain' sep dom+ | BS.null dom || dom == rootDomain = put8 0+ | otherwise = do+ mpos <- wsPop dom+ cur <- gets wsPosition+ case mpos of+ Just pos -> putPointer pos+ Nothing -> do+ -- Pointers are limited to 14-bits!+ when (cur <= 0x3fff) $ wsPush dom cur+ mconcat [ putPartialDomain hd+ , putDomain' '.' tl+ ]+ where+ -- Try with the preferred separator if present, else fall back to '.'.+ (hd, tl) = loop (c2w sep)+ where+ loop w = case parseLabel w dom of+ Right p | w /= 0x2e && BS.null (snd p) -> loop 0x2e+ | otherwise -> p+ Left e -> E.throw e++ c2w = fromIntegral . fromEnum++putPointer :: Int -> SPut+putPointer pos = putInt16 (pos .|. 0xc000)++putPartialDomain :: Domain -> SPut+putPartialDomain = putByteStringWithLength
+ internal/Network/DNS/Encode/Internal.hs view
@@ -0,0 +1,38 @@+{-# OPTIONS_HADDOCK hide #-}++-- | Internal DNS message component encoders for the test-suite.+module Network.DNS.Encode.Internal (+ encodeDNSHeader+ , encodeDNSFlags+ , encodeDomain+ , encodeMailbox+ , encodeResourceRecord+ ) where++import Network.DNS.Encode.Builders+import Network.DNS.Imports+import Network.DNS.StateBinary+import Network.DNS.Types.Internal++-- | Encode DNS flags.+encodeDNSFlags :: DNSFlags -> ByteString+encodeDNSFlags = runSPut . putDNSFlags++-- | Encode DNS header.+encodeDNSHeader :: DNSHeader -> ByteString+encodeDNSHeader = runSPut . putHeader++-- | Encode a domain.+encodeDomain :: Domain -> ByteString+encodeDomain = runSPut . putDomain++-- | Encode a mailbox name. The first label is separated from the remaining+-- labels by an @'\@'@ rather than a @.@. This is used for the contact+-- address in the @SOA@ and @RP@ records.+--+encodeMailbox :: Mailbox -> ByteString+encodeMailbox = runSPut . putMailbox++-- | Encode a ResourceRecord.+encodeResourceRecord :: ResourceRecord -> ByteString+encodeResourceRecord rr = runSPut $ putResourceRecord rr
+ internal/Network/DNS/Imports.hs view
@@ -0,0 +1,31 @@+module Network.DNS.Imports (+ ByteString+ , Int64+ , NonEmpty(..)+ , module Control.Applicative+ , module Control.Monad+ , module Data.Bits+ , module Data.Function+ , module Data.List+ , module Data.Maybe+ , module Data.Monoid+ , module Data.Ord+ , module Data.Typeable+ , module Data.Word+ , module Numeric+ ) where++import Control.Applicative+import Control.Monad+import Data.Bits+import Data.ByteString (ByteString)+import Data.Function+import Data.Int (Int64)+import Data.List+import Data.List.NonEmpty (NonEmpty(..))+import Data.Maybe+import Data.Monoid+import Data.Ord+import Data.Typeable+import Data.Word+import Numeric
+ internal/Network/DNS/Memo.hs view
@@ -0,0 +1,102 @@+{-# LANGUAGE RecordWildCards #-}++module Network.DNS.Memo where++import qualified Control.Reaper as R+import qualified Data.ByteString as B+import qualified Data.CaseInsensitive as CI+import Data.Hourglass (Elapsed)+import Data.OrdPSQ (OrdPSQ)+import qualified Data.OrdPSQ as PSQ+import Time.System (timeCurrent)++import Network.DNS.Imports+import Network.DNS.Types.Internal++data Section = Answer | Authority deriving (Eq, Ord, Show)++type Key = (ByteString+ ,TYPE)+type Prio = Elapsed++type Entry = Either DNSError [RData]++type DB = OrdPSQ Key Prio Entry++type Cache = R.Reaper DB (Key,Prio,Entry)++newCache :: Int -> IO Cache+newCache delay = R.mkReaper R.defaultReaperSettings {+ R.reaperEmpty = PSQ.empty+ , R.reaperCons = \(k, tim, v) psq -> PSQ.insert k tim v psq+ , R.reaperAction = prune+ , R.reaperDelay = delay * 1000000+ , R.reaperNull = PSQ.null+ }++lookupCache :: Key -> Cache -> IO (Maybe (Prio, Entry))+lookupCache key reaper = PSQ.lookup key <$> R.reaperRead reaper++insertCache :: Key -> Prio -> Entry -> Cache -> IO ()+insertCache (dom,typ) tim ent0 reaper = R.reaperAdd reaper (key,tim,ent)+ where+ key = (B.copy dom,typ)+ ent = case ent0 of+ l@(Left _) -> l+ (Right rds) -> Right $ map copy rds++-- Theoretically speaking, atMostView itself is good enough for pruning.+-- But auto-update assumes a list based db which does not provide atMost+-- functions. So, we need to do this redundant way.+prune :: DB -> IO (DB -> DB)+prune oldpsq = do+ tim <- timeCurrent+ let (_, pruned) = PSQ.atMostView tim oldpsq+ return $ \newpsq -> foldl' ins pruned $ PSQ.toList newpsq+ where+ ins psq (k,p,v) = PSQ.insert k p v psq++copy :: RData -> RData+copy r@(RD_A _) = r+copy (RD_NS dom) = RD_NS $ B.copy dom+copy (RD_CNAME dom) = RD_CNAME $ B.copy dom+copy (RD_SOA mn mr a b c d e) = RD_SOA (B.copy mn) (B.copy mr) a b c d e+copy (RD_PTR dom) = RD_PTR $ B.copy dom+copy (RD_NULL bytes) = RD_NULL $ B.copy bytes+copy (RD_MX prf dom) = RD_MX prf $ B.copy dom+copy (RD_TXT txt) = RD_TXT $ B.copy txt+copy (RD_RP mbox dname) = RD_RP (B.copy mbox) (B.copy dname)+copy r@(RD_AAAA _) = r+copy (RD_SRV a b c dom) = RD_SRV a b c $ B.copy dom+copy (RD_DNAME dom) = RD_DNAME $ B.copy dom+copy (RD_OPT od) = RD_OPT $ map copyOData od+copy (RD_DS t a dt dv) = RD_DS t a dt $ B.copy dv+copy (RD_CDS t a dt dv) = RD_CDS t a dt $ B.copy dv+copy (RD_NSEC dom ts) = RD_NSEC (B.copy dom) ts+copy (RD_DNSKEY f p a k) = RD_DNSKEY f p a $ B.copy k+copy (RD_CDNSKEY f p a k) = RD_CDNSKEY f p a $ B.copy k+copy (RD_TLSA a b c dgst) = RD_TLSA a b c $ B.copy dgst+copy (RD_NSEC3 a b c s h t) = RD_NSEC3 a b c (B.copy s) (B.copy h) t+copy (RD_NSEC3PARAM a b c salt) = RD_NSEC3PARAM a b c $ B.copy salt+copy (RD_RRSIG sig) = RD_RRSIG $ copysig sig+ where+ copysig s@RDREP_RRSIG{..} =+ s { rrsigZone = B.copy rrsigZone+ , rrsigValue = B.copy rrsigValue }+copy (RD_CAA f t v) = RD_CAA f (CI.mk (B.copy (CI.original t))) (B.copy v)+copy (UnknownRData is) = UnknownRData $ B.copy is++copyOData :: OData -> OData+copyOData (OD_ECSgeneric family srcBits scpBits bs) =+ OD_ECSgeneric family srcBits scpBits $ B.copy bs+copyOData (OD_NSID nsid) = OD_NSID $ B.copy nsid+copyOData (UnknownOData c b) = UnknownOData c $ B.copy b++-- No copying required for the rest, but avoiding a wildcard pattern match+-- so that if more option types are added in the future, the compiler will+-- complain about a partial function.+--+copyOData o@OD_ClientSubnet {} = o+copyOData o@OD_DAU {} = o+copyOData o@OD_DHU {} = o+copyOData o@OD_N3U {} = o
+ internal/Network/DNS/Resolver/Internal.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE CPP #-}++module Network.DNS.Resolver.Internal (+ getDefaultDnsServers+ ) where++import Network.DNS.Imports++#if !defined(mingw32_HOST_OS)+#define POSIX+#else+#define WIN+#endif++#if defined(WIN)+import Foreign.C.String+import Foreign.Marshal.Alloc (allocaBytes)+#else+import Data.Char (isSpace)+#endif++getDefaultDnsServers :: FilePath -> IO [String]++#if defined(WIN)++foreign import ccall "getWindowsDefDnsServers" getWindowsDefDnsServers :: CString -> Int -> IO Word32++getDefaultDnsServers _ = do+ allocaBytes 256 $ \cString -> do+ res <- getWindowsDefDnsServers cString 256+ case res of+ 0 -> split ',' <$> peekCString cString+ _ -> return [] -- TODO: Do proper error handling here.+ where+ split :: Char -> String -> [String]+ split c cs =+ let (h, t) = dropWhile (== c) <$> break (== c) cs+ in if null t+ then if null h then [] else [h]+ else if null h+ then split c t+ else h : split c t++#else++getDefaultDnsServers file = toAddresses <$> readFile file+ where+ toAddresses :: String -> [String]+ toAddresses cs = map extract (filter ("nameserver" `isPrefixOf`) (lines cs))+ extract = reverse . dropWhile isSpace . reverse . dropWhile isSpace . drop 11++#endif
+ internal/Network/DNS/StateBinary.hs view
@@ -0,0 +1,441 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-}+module Network.DNS.StateBinary (+ PState(..)+ , initialState+ , SPut+ , runSPut+ , put8+ , put16+ , put32+ , putInt8+ , putInt16+ , putInt32+ , putByteString+ , putReplicate+ , SGet+ , failSGet+ , fitSGet+ , runSGet+ , runSGetAt+ , runSGetWithLeftovers+ , runSGetWithLeftoversAt+ , get8+ , get16+ , get32+ , getInt8+ , getInt16+ , getInt32+ , getNByteString+ , sGetMany+ , getPosition+ , getInput+ , getAtTime+ , wsPop+ , wsPush+ , wsPosition+ , addPositionW+ , push+ , pop+ , getNBytes+ , getNoctets+ , skipNBytes+ , parseLabel+ , unparseLabel+ ) where++import qualified Control.Exception as E+import Control.Monad.State.Strict (State, StateT)+import qualified Control.Monad.State.Strict as ST+import qualified Data.Attoparsec.ByteString as A+import qualified Data.Attoparsec.Types as T+import qualified Data.ByteString as BS+import Data.ByteString.Builder (Builder)+import qualified Data.ByteString.Builder as BB+import qualified Data.ByteString.Char8 as S8+import qualified Data.ByteString.Lazy as LB+import qualified Data.ByteString.Lazy.Char8 as LBS+import Data.IntMap (IntMap)+import qualified Data.IntMap as IM+import Data.Map (Map)+import qualified Data.Map as M+import Data.Semigroup as Sem++import Network.DNS.Imports+import Network.DNS.Types.Internal++----------------------------------------------------------------++type SPut = State WState Builder++data WState = WState {+ wsDomain :: Map Domain Int+ , wsPosition :: Int+}++initialWState :: WState+initialWState = WState M.empty 0++instance Sem.Semigroup SPut where+ p1 <> p2 = (Sem.<>) <$> p1 <*> p2++instance Monoid SPut where+ mempty = return mempty+#if !(MIN_VERSION_base(4,11,0))+ mappend = (Sem.<>)+#endif++put8 :: Word8 -> SPut+put8 = fixedSized 1 BB.word8++put16 :: Word16 -> SPut+put16 = fixedSized 2 BB.word16BE++put32 :: Word32 -> SPut+put32 = fixedSized 4 BB.word32BE++putInt8 :: Int -> SPut+putInt8 = fixedSized 1 (BB.int8 . fromIntegral)++putInt16 :: Int -> SPut+putInt16 = fixedSized 2 (BB.int16BE . fromIntegral)++putInt32 :: Int -> SPut+putInt32 = fixedSized 4 (BB.int32BE . fromIntegral)++putByteString :: ByteString -> SPut+putByteString = writeSized BS.length BB.byteString++putReplicate :: Int -> Word8 -> SPut+putReplicate n w =+ fixedSized n BB.lazyByteString $ LB.replicate (fromIntegral n) w++addPositionW :: Int -> State WState ()+addPositionW n = do+ (WState m cur) <- ST.get+ ST.put $ WState m (cur+n)++fixedSized :: Int -> (a -> Builder) -> a -> SPut+fixedSized n f a = do addPositionW n+ return (f a)++writeSized :: (a -> Int) -> (a -> Builder) -> a -> SPut+writeSized n f a = do addPositionW (n a)+ return (f a)++wsPop :: Domain -> State WState (Maybe Int)+wsPop dom = do+ doms <- ST.gets wsDomain+ return $ M.lookup dom doms++wsPush :: Domain -> Int -> State WState ()+wsPush dom pos = do+ (WState m cur) <- ST.get+ ST.put $ WState (M.insert dom pos m) cur++----------------------------------------------------------------++type SGet = StateT PState (T.Parser ByteString)++data PState = PState {+ psDomain :: IntMap Domain+ , psPosition :: Int+ , psInput :: ByteString+ , psAtTime :: Int64+ }++----------------------------------------------------------------++getPosition :: SGet Int+getPosition = ST.gets psPosition++getInput :: SGet ByteString+getInput = ST.gets psInput++getAtTime :: SGet Int64+getAtTime = ST.gets psAtTime++addPosition :: Int -> SGet ()+addPosition n | n < 0 = failSGet "internal error: negative position increment"+ | otherwise = do+ PState dom pos inp t <- ST.get+ let !pos' = pos + n+ when (pos' > BS.length inp) $+ failSGet "malformed or truncated input"+ ST.put $ PState dom pos' inp t++push :: Int -> Domain -> SGet ()+push n d = do+ PState dom pos inp t <- ST.get+ ST.put $ PState (IM.insert n d dom) pos inp t++pop :: Int -> SGet (Maybe Domain)+pop n = ST.gets (IM.lookup n . psDomain)++----------------------------------------------------------------++get8 :: SGet Word8+get8 = ST.lift A.anyWord8 <* addPosition 1++get16 :: SGet Word16+get16 = ST.lift getWord16be <* addPosition 2+ where+ word8' = fromIntegral <$> A.anyWord8+ getWord16be = do+ a <- word8'+ b <- word8'+ return $ a * 0x100 + b++get32 :: SGet Word32+get32 = ST.lift getWord32be <* addPosition 4+ where+ word8' = fromIntegral <$> A.anyWord8+ getWord32be = do+ a <- word8'+ b <- word8'+ c <- word8'+ d <- word8'+ return $ a * 0x1000000 + b * 0x10000 + c * 0x100 + d++getInt8 :: SGet Int+getInt8 = fromIntegral <$> get8++getInt16 :: SGet Int+getInt16 = fromIntegral <$> get16++getInt32 :: SGet Int+getInt32 = fromIntegral <$> get32++----------------------------------------------------------------++overrun :: SGet a+overrun = failSGet "malformed or truncated input"++getNBytes :: Int -> SGet [Int]+getNBytes n | n < 0 = overrun+ | otherwise = toInts <$> getNByteString n+ where+ toInts = map fromIntegral . BS.unpack++getNoctets :: Int -> SGet [Word8]+getNoctets n | n < 0 = overrun+ | otherwise = BS.unpack <$> getNByteString n++skipNBytes :: Int -> SGet ()+skipNBytes n | n < 0 = overrun+ | otherwise = ST.lift (A.take n) >> addPosition n++getNByteString :: Int -> SGet ByteString+getNByteString n | n < 0 = overrun+ | otherwise = ST.lift (A.take n) <* addPosition n++fitSGet :: Int -> SGet a -> SGet a+fitSGet len parser | len < 0 = overrun+ | otherwise = do+ pos0 <- getPosition+ ret <- parser+ pos' <- getPosition+ if pos' == pos0 + len+ then return $! ret+ else if pos' > pos0 + len+ then failSGet "element size exceeds declared size"+ else failSGet "element shorter than declared size"++-- | Parse a list of elements that takes up exactly a given number of bytes.+-- In order to avoid infinite loops, if an element parser succeeds without+-- moving the buffer offset forward, an error will be returned.+--+sGetMany :: String -- ^ element type for error messages+ -> Int -- ^ input buffer length+ -> SGet a -- ^ element parser+ -> SGet [a]+sGetMany elemname len parser | len < 0 = overrun+ | otherwise = go len []+ where+ go n xs+ | n < 0 = failSGet $ elemname ++ " longer than declared size"+ | n == 0 = pure $ reverse xs+ | otherwise = do+ pos0 <- getPosition+ x <- parser+ pos1 <- getPosition+ if pos1 <= pos0+ then failSGet $ "internal error: in-place success for " ++ elemname+ else go (n + pos0 - pos1) (x : xs)++----------------------------------------------------------------++-- | To get a broad range of correct RRSIG inception and expiration times+-- without over or underflow, we choose a time half way between midnight PDT+-- 2010-07-15 (the day the root zone was signed) and 2^32 seconds later on+-- 2146-08-21. Since 'decode' and 'runSGet' are pure, we can't peek at the+-- current time while parsing. Outside this date range the output is off by+-- some non-zero multiple 2\^32 seconds.+--+dnsTimeMid :: Int64+dnsTimeMid = 3426660848++initialState :: Int64 -> ByteString -> PState+initialState t inp = PState IM.empty 0 inp t++-- Construct our own error message, without the unhelpful AttoParsec+-- \"Failed reading: \" prefix.+--+failSGet :: String -> SGet a+failSGet msg = ST.lift (fail "" A.<?> msg)++runSGetAt :: Int64 -> SGet a -> ByteString -> Either DNSError (a, PState)+runSGetAt t parser inp =+ toResult $ A.parse (ST.runStateT parser $ initialState t inp) inp+ where+ toResult :: A.Result r -> Either DNSError r+ toResult (A.Done _ r) = Right r+ toResult (A.Fail _ ctx msg) = Left $ DecodeError $ head $ ctx ++ [msg]+ toResult (A.Partial _) = Left $ DecodeError "incomplete input"++runSGet :: SGet a -> ByteString -> Either DNSError (a, PState)+runSGet = runSGetAt dnsTimeMid++runSGetWithLeftoversAt :: Int64 -- ^ Reference time for DNS clock arithmetic+ -> SGet a -- ^ Parser+ -> ByteString -- ^ Encoded message+ -> Either DNSError ((a, PState), ByteString)+runSGetWithLeftoversAt t parser inp =+ toResult $ A.parse (ST.runStateT parser $ initialState t inp) inp+ where+ toResult :: A.Result r -> Either DNSError (r, ByteString)+ toResult (A.Done i r) = Right (r, i)+ toResult (A.Partial f) = toResult $ f BS.empty+ toResult (A.Fail _ ctx e) = Left $ DecodeError $ head $ ctx ++ [e]++runSGetWithLeftovers :: SGet a -> ByteString -> Either DNSError ((a, PState), ByteString)+runSGetWithLeftovers = runSGetWithLeftoversAt dnsTimeMid++runSPut :: SPut -> ByteString+runSPut = LBS.toStrict . BB.toLazyByteString . flip ST.evalState initialWState++----------------------------------------------------------------++-- | Decode a domain name in A-label form to a leading label and a tail with+-- the remaining labels, unescaping backlashed chars and decimal triples along+-- the way. Any U-label conversion belongs at the layer above this code.+--+parseLabel :: Word8 -> ByteString -> Either DNSError (ByteString, ByteString)+parseLabel sep dom =+ if BS.any (== bslash) dom+ then toResult $ A.parse (labelParser sep mempty) dom+ else check $ safeTail <$> BS.break (== sep) dom+ where+ toResult (A.Partial c) = toResult (c mempty)+ toResult (A.Done tl hd) = check (hd, tl)+ toResult _ = bottom+ safeTail bs | BS.null bs = mempty+ | otherwise = BS.tail bs+ check r@(hd, tl) | not (BS.null hd) || BS.null tl = Right r+ | otherwise = bottom+ bottom = Left $ DecodeError $ "invalid domain: " ++ S8.unpack dom++labelParser :: Word8 -> ByteString -> A.Parser ByteString+labelParser sep acc = do+ acc' <- mappend acc <$> A.option mempty simple+ labelEnd sep acc' <|> (escaped >>= labelParser sep . BS.snoc acc')+ where+ simple = fst <$> A.match skipUnescaped+ where+ skipUnescaped = A.skipMany1 $ A.satisfy notSepOrBslash+ notSepOrBslash w = w /= sep && w /= bslash++ escaped = do+ A.skip (== bslash)+ either decodeDec pure =<< A.eitherP digit A.anyWord8+ where+ digit = fromIntegral <$> A.satisfyWith (\n -> n - zero) (<=9)+ decodeDec d =+ safeWord8 =<< trigraph d <$> digit <*> digit+ where+ trigraph :: Word -> Word -> Word -> Word+ trigraph x y z = 100 * x + 10 * y + z++ safeWord8 :: Word -> A.Parser Word8+ safeWord8 n | n > 255 = mzero+ | otherwise = pure $ fromIntegral n++labelEnd :: Word8 -> ByteString -> A.Parser ByteString+labelEnd sep acc =+ A.satisfy (== sep) *> pure acc <|>+ A.endOfInput *> pure acc++----------------------------------------------------------------++-- | Convert a wire-form label to presentation-form by escaping+-- the separator, special and non-printing characters. For simple+-- labels with no bytes that require escaping we get back the input+-- bytestring asis with no copying or re-construction.+--+-- Note: the separator is required to be either \'.\' or \'\@\', but this+-- constraint is the caller's responsibility and is not checked here.+--+unparseLabel :: Word8 -> ByteString -> ByteString+unparseLabel sep label =+ if BS.all (isPlain sep) label+ then label+ else toResult $ A.parse (labelUnparser sep mempty) label+ where+ toResult (A.Partial c) = toResult (c mempty)+ toResult (A.Done _ r) = r+ toResult _ = E.throw UnknownDNSError -- can't happen++labelUnparser :: Word8 -> ByteString -> A.Parser ByteString+labelUnparser sep acc = do+ acc' <- mappend acc <$> A.option mempty asis+ A.endOfInput *> pure acc' <|> (esc >>= labelUnparser sep . mappend acc')+ where+ -- Non-printables are escaped as decimal trigraphs, while printable+ -- specials just get a backslash prefix.+ esc = do+ w <- A.anyWord8+ if w <= 32 || w >= 127+ then let (q100, r100) = w `divMod` 100+ (q10, r10) = r100 `divMod` 10+ in pure $ BS.pack [ bslash, zero + q100, zero + q10, zero + r10 ]+ else pure $ BS.pack [ bslash, w ]++ -- Runs of plain bytes are recognized as a single chunk, which is then+ -- returned as-is.+ asis = fmap fst $ A.match $ A.skipMany1 $ A.satisfy $ isPlain sep++-- | In the presentation form of DNS labels, these characters are escaped by+-- prepending a backlash. (They have special meaning in zone files). Whitespace+-- and other non-printable or non-ascii characters are encoded via "\DDD"+-- decimal escapes. The separator character is also quoted in each label. Note+-- that '@' is quoted even when not the separator.+escSpecials :: ByteString+escSpecials = "\"$();@\\"++-- | Is the given byte the separator or one of the specials?+isSpecial :: Word8 -> Word8 -> Bool+isSpecial sep w = w == sep || BS.elemIndex w escSpecials /= Nothing++-- | Is the given byte a plain byte that reqires no escaping. The tests are+-- ordered to succeed or fail quickly in the most common cases. The test+-- ranges assume the expected numeric values of the named special characters.+-- Note: the separator is assumed to be either '.' or '@' and so not matched by+-- any of the first three fast-path 'True' cases.+isPlain :: Word8 -> Word8 -> Bool+isPlain sep w | w >= 127 = False -- <DEL> + non-ASCII+ | w > bslash = True -- ']'..'_'..'a'..'z'..'~'+ | w >= zero && w < semi = True -- '0'..'9'..':'+ | w > atsign && w < bslash = True -- 'A'..'Z'..'['+ | w <= 32 = False -- non-printables+ | isSpecial sep w = False -- one of the specials+ | otherwise = True -- plain punctuation++-- | Some numeric byte constants.+zero, semi, atsign, bslash :: Word8+zero = fromIntegral $ fromEnum '0' -- 48+semi = fromIntegral $ fromEnum ';' -- 59+atsign = fromIntegral $ fromEnum '@' -- 64+bslash = fromIntegral $ fromEnum '\\' -- 92
+ internal/Network/DNS/Types/Internal.hs view
@@ -0,0 +1,1407 @@+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE CPP #-}++module Network.DNS.Types.Internal where++import Control.Exception (Exception, IOException)+import qualified Data.ByteString as B+import qualified Data.ByteString.Base16 as B16+import qualified Data.ByteString.Base64 as B64+import qualified Data.ByteString.Char8 as BS+import Data.CaseInsensitive (CI)+import qualified Data.CaseInsensitive as CI+import Data.Char (intToDigit)+import qualified Data.Hourglass as H+import Data.IP (IP(..), IPv4, IPv6)+import qualified Data.Semigroup as Sem++import qualified Network.DNS.Base32Hex as B32+import Network.DNS.Imports++-- $setup+-- >>> import Network.DNS++----------------------------------------------------------------++-- | This type holds the /presentation form/ of fully-qualified DNS domain+-- names encoded as ASCII A-labels, with \'.\' separators between labels.+-- Non-printing characters are escaped as @\\DDD@ (a backslash, followed by+-- three decimal digits). The special characters: @ \", \$, (, ), ;, \@,@ and+-- @\\@ are escaped by prepending a backslash. The trailing \'.\' is optional+-- on input, but is recommended, and is always added when decoding from+-- /wire form/.+--+-- The encoding of domain names to /wire form/, e.g. for transmission in a+-- query, requires the input encodings to be valid, otherwise a 'DecodeError'+-- may be thrown. Domain names received in wire form in DNS messages are+-- escaped to this presentation form as part of decoding the 'DNSMessage'.+--+-- This form is ASCII-only. Any conversion between A-label 'ByteString's,+-- and U-label 'Text' happens at whatever layer maps user input to DNS+-- names, or presents /friendly/ DNS names to the user. Not all users+-- can read all scripts, and applications that default to U-label form+-- should ideally give the user a choice to see the A-label form.+-- Examples:+--+-- @+-- www.example.org. -- Ordinary DNS name.+-- \_25.\_tcp.mx1.example.net. -- TLSA RR initial labels have \_ prefixes.+-- \\001.exotic.example. -- First label is Ctrl-A!+-- just\\.one\\.label.example. -- First label is \"just.one.label\"+-- @+--+type Domain = ByteString++-- | Type for a mailbox encoded on the wire as a DNS name, but the first label+-- is conceptually the local part of an email address, and may contain internal+-- periods that are not label separators. Therefore, in mailboxes \@ is used as+-- the separator between the first and second labels, and any \'.\' characters+-- in the first label are not escaped. The encoding is otherwise the same as+-- 'Domain' above. This is most commonly seen in the /rname/ of @SOA@ records,+-- and is also employed in the @mbox-dname@ field of @RP@ records.+-- On input, if there is no unescaped \@ character in the 'Mailbox', it is+-- reparsed with \'.\' as the first label separator. Thus the traditional+-- format with all labels separated by dots is also accepted, but decoding from+-- wire form always uses \@ between the first label and the domain-part of the+-- address. Examples:+--+-- @+-- hostmaster\@example.org. -- First label is simply @hostmaster@+-- john.smith\@examle.com. -- First label is @john.smith@+-- @+--+type Mailbox = ByteString++----------------------------------------------------------------++-- | Types for resource records.+newtype TYPE = TYPE {+ -- | From type to number.+ fromTYPE :: Word16+ } deriving (Eq, Ord)++-- https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4++-- | IPv4 address+pattern A :: TYPE+pattern A = TYPE 1+-- | An authoritative name serve+pattern NS :: TYPE+pattern NS = TYPE 2+-- | The canonical name for an alias+pattern CNAME :: TYPE+pattern CNAME = TYPE 5+-- | Marks the start of a zone of authority+pattern SOA :: TYPE+pattern SOA = TYPE 6+-- | A null RR (EXPERIMENTAL)+pattern NULL :: TYPE+pattern NULL = TYPE 10+-- | A domain name pointer+pattern PTR :: TYPE+pattern PTR = TYPE 12+-- | Mail exchange+pattern MX :: TYPE+pattern MX = TYPE 15+-- | Text strings+pattern TXT :: TYPE+pattern TXT = TYPE 16+-- | Responsible Person+pattern RP :: TYPE+pattern RP = TYPE 17+-- | IPv6 Address+pattern AAAA :: TYPE+pattern AAAA = TYPE 28+-- | Server Selection (RFC2782)+pattern SRV :: TYPE+pattern SRV = TYPE 33+-- | DNAME (RFC6672)+pattern DNAME :: TYPE+pattern DNAME = TYPE 39 -- RFC 6672+-- | OPT (RFC6891)+pattern OPT :: TYPE+pattern OPT = TYPE 41 -- RFC 6891+-- | Delegation Signer (RFC4034)+pattern DS :: TYPE+pattern DS = TYPE 43 -- RFC 4034+-- | RRSIG (RFC4034)+pattern RRSIG :: TYPE+pattern RRSIG = TYPE 46 -- RFC 4034+-- | NSEC (RFC4034)+pattern NSEC :: TYPE+pattern NSEC = TYPE 47 -- RFC 4034+-- | DNSKEY (RFC4034)+pattern DNSKEY :: TYPE+pattern DNSKEY = TYPE 48 -- RFC 4034+-- | NSEC3 (RFC5155)+pattern NSEC3 :: TYPE+pattern NSEC3 = TYPE 50 -- RFC 5155+-- | NSEC3PARAM (RFC5155)+pattern NSEC3PARAM :: TYPE+pattern NSEC3PARAM = TYPE 51 -- RFC 5155+-- | TLSA (RFC6698)+pattern TLSA :: TYPE+pattern TLSA = TYPE 52 -- RFC 6698+-- | Child DS (RFC7344)+pattern CDS :: TYPE+pattern CDS = TYPE 59 -- RFC 7344+-- | DNSKEY(s) the Child wants reflected in DS (RFC7344)+pattern CDNSKEY :: TYPE+pattern CDNSKEY = TYPE 60 -- RFC 7344+-- | Child-To-Parent Synchronization (RFC7477)+pattern CSYNC :: TYPE+pattern CSYNC = TYPE 62 -- RFC 7477+-- | Zone transfer (RFC5936)+pattern AXFR :: TYPE+pattern AXFR = TYPE 252 -- RFC 5936+-- | A request for all records the server/cache has available+pattern ANY :: TYPE+pattern ANY = TYPE 255+-- | Certification Authority Authorization (RFC6844)+pattern CAA :: TYPE+pattern CAA = TYPE 257 -- RFC 6844++-- | From number to type.+toTYPE :: Word16 -> TYPE+toTYPE = TYPE++instance Show TYPE where+ show A = "A"+ show NS = "NS"+ show CNAME = "CNAME"+ show SOA = "SOA"+ show NULL = "NULL"+ show PTR = "PTR"+ show MX = "MX"+ show TXT = "TXT"+ show RP = "RP"+ show AAAA = "AAAA"+ show SRV = "SRV"+ show DNAME = "DNAME"+ show OPT = "OPT"+ show DS = "DS"+ show RRSIG = "RRSIG"+ show NSEC = "NSEC"+ show DNSKEY = "DNSKEY"+ show NSEC3 = "NSEC3"+ show NSEC3PARAM = "NSEC3PARAM"+ show TLSA = "TLSA"+ show CDS = "CDS"+ show CDNSKEY = "CDNSKEY"+ show CSYNC = "CSYNC"+ show AXFR = "AXFR"+ show ANY = "ANY"+ show CAA = "CAA"+ show x = "TYPE" ++ show (fromTYPE x)++----------------------------------------------------------------++-- | An enumeration of all possible DNS errors that can occur.+data DNSError =+ -- | The sequence number of the answer doesn't match our query. This+ -- could indicate foul play.+ SequenceNumberMismatch+ -- | The question section of the response doesn't match our query. This+ -- could indicate foul play.+ | QuestionMismatch+ -- | A zone tranfer, i.e., a request of type AXFR, was attempted with the+ -- "lookup" interface. Zone transfer is different enough from "normal"+ -- requests that it requires a different interface.+ | InvalidAXFRLookup+ -- | The number of retries for the request was exceeded.+ | RetryLimitExceeded+ -- | TCP fallback request timed out.+ | TimeoutExpired+ -- | The answer has the correct sequence number, but returned an+ -- unexpected RDATA format.+ | UnexpectedRDATA+ -- | The domain for query is illegal.+ | IllegalDomain+ -- | The name server was unable to interpret the query.+ | FormatError+ -- | The name server was unable to process this query due to a+ -- problem with the name server.+ | ServerFailure+ -- | This code signifies that the domain name referenced in the+ -- query does not exist.+ | NameError+ -- | The name server does not support the requested kind of query.+ | NotImplemented+ -- | The name server refuses to perform the specified operation for+ -- policy reasons. For example, a name+ -- server may not wish to provide the+ -- information to the particular requester,+ -- or a name server may not wish to perform+ -- a particular operation (e.g., zone transfer) for particular data.+ | OperationRefused+ -- | The server does not support the OPT RR version or content+ | BadOptRecord+ -- | Configuration is wrong.+ | BadConfiguration+ -- | Network failure.+ | NetworkFailure IOException+ -- | Error is unknown+ | DecodeError String+ | UnknownDNSError+ deriving (Eq, Show, Typeable)++instance Exception DNSError+++-- | Data type representing the optional EDNS pseudo-header of a 'DNSMessage'+-- When a single well-formed @OPT@ 'ResourceRecord' was present in the+-- message's additional section, it is decoded to an 'EDNS' record and and+-- stored in the message 'ednsHeader' field. The corresponding @OPT RR@ is+-- then removed from the additional section.+--+-- When the constructor is 'NoEDNS', no @EDNS OPT@ record was present in the+-- message additional section. When 'InvalidEDNS', the message holds either a+-- malformed OPT record or more than one OPT record, which can still be found+-- in (have not been removed from) the message additional section.+--+-- The EDNS OPT record augments the message error status with an 8-bit field+-- that forms 12-bit extended RCODE when combined with the 4-bit RCODE from the+-- unextended DNS header. In EDNS messages it is essential to not use just the+-- bare 4-bit 'RCODE' from the original DNS header. Therefore, in order to+-- avoid potential misinterpretation of the response 'RCODE', when the OPT+-- record is decoded, the upper eight bits of the error status are+-- automatically combined with the 'rcode' of the message header, so that there+-- is only one place in which to find the full 12-bit result. Therefore, the+-- decoded 'EDNS' pseudo-header, does not hold any error status bits.+--+-- The reverse process occurs when encoding messages. The low four bits of the+-- message header 'rcode' are encoded into the wire-form DNS header, while the+-- upper eight bits are encoded as part of the OPT record. In DNS responses with+-- an 'rcode' larger than 15, EDNS extensions SHOULD be enabled by providing a+-- value for 'ednsHeader' with a constructor of 'EDNSheader'. If EDNS is not+-- enabled in such a message, in order to avoid truncation of 'RCODE' values+-- that don't fit in the non-extended DNS header, the encoded wire-form 'RCODE'+-- is set to 'FormatErr'.+--+-- When encoding messages for transmission, the 'ednsHeader' is used to+-- generate the additional OPT record. Do not add explicit @OPT@ records+-- to the aditional section, configure EDNS via the 'EDNSheader' instead.+--+-- >>> let getopts eh = mapEDNS eh ednsOptions []+-- >>> let optsin = [OD_ClientSubnet 24 0 $ read "192.0.2.1"]+-- >>> let masked = [OD_ClientSubnet 24 0 $ read "192.0.2.0"]+-- >>> let message = makeEmptyQuery $ ednsSetOptions $ ODataSet optsin+-- >>> let optsout = getopts. ednsHeader <$> (decode $ encode message)+-- >>> optsout == Right masked+-- True+--+data EDNSheader = EDNSheader EDNS -- ^ A valid EDNS message+ | NoEDNS -- ^ A valid non-EDNS message+ | InvalidEDNS -- ^ Multiple or bad additional @OPT@ RRs+ deriving (Eq, Show)+++-- | Return the second argument for EDNS messages, otherwise the third.+ifEDNS :: EDNSheader -- ^ EDNS pseudo-header+ -> a -- ^ Value to return for EDNS messages+ -> a -- ^ Value to return for non-EDNS messages+ -> a+ifEDNS (EDNSheader _) a _ = a+ifEDNS _ _ b = b+{-# INLINE ifEDNS #-}+++-- | Return the output of a function applied to the EDNS pseudo-header if EDNS+-- is enabled, otherwise return a default value.+mapEDNS :: EDNSheader -- ^ EDNS pseudo-header+ -> (EDNS -> a) -- ^ Function to apply to 'EDNS' value+ -> a -- ^ Default result for non-EDNS messages+ -> a+mapEDNS (EDNSheader eh) f _ = f eh+mapEDNS _ _ a = a+{-# INLINE mapEDNS #-}+++-- | DNS message format for queries and replies.+--+data DNSMessage = DNSMessage {+ header :: !DNSHeader -- ^ Header with extended 'RCODE'+ , ednsHeader :: EDNSheader -- ^ EDNS pseudo-header+ , question :: [Question] -- ^ The question for the name server+ , answer :: Answers -- ^ RRs answering the question+ , authority :: AuthorityRecords -- ^ RRs pointing toward an authority+ , additional :: AdditionalRecords -- ^ RRs holding additional information+ } deriving (Eq, Show)++-- | An identifier assigned by the program that+-- generates any kind of query.+type Identifier = Word16++-- | Raw data format for the header of DNS Query and Response.+data DNSHeader = DNSHeader {+ identifier :: !Identifier -- ^ Query or reply identifier.+ , flags :: !DNSFlags -- ^ Flags, OPCODE, and RCODE+ } deriving (Eq, Show)++-- | Raw data format for the flags of DNS Query and Response.+data DNSFlags = DNSFlags {+ qOrR :: !QorR -- ^ Query or response.+ , opcode :: !OPCODE -- ^ Kind of query.+ , authAnswer :: !Bool -- ^ AA (Authoritative Answer) bit - this bit is valid in responses,+ -- and specifies that the responding name server is an+ -- authority for the domain name in question section.+ , trunCation :: !Bool -- ^ TC (Truncated Response) bit - specifies that this message was truncated+ -- due to length greater than that permitted on the+ -- transmission channel.+ , recDesired :: !Bool -- ^ RD (Recursion Desired) bit - this bit may be set in a query and+ -- is copied into the response. If RD is set, it directs+ -- the name server to pursue the query recursively.+ -- Recursive query support is optional.+ , recAvailable :: !Bool -- ^ RA (Recursion Available) bit - this be is set or cleared in a+ -- response, and denotes whether recursive query support is+ -- available in the name server.++ , rcode :: !RCODE -- ^ The full 12-bit extended RCODE when EDNS is in use.+ -- Should always be zero in well-formed requests.+ -- When decoding replies, the high eight bits from+ -- any EDNS response are combined with the 4-bit+ -- RCODE from the DNS header. When encoding+ -- replies, if no EDNS OPT record is provided, RCODE+ -- values > 15 are mapped to 'FormatErr'.+ , authenData :: !Bool -- ^ AD (Authenticated Data) bit - (RFC4035, Section 3.2.3).+ , chkDisable :: !Bool -- ^ CD (Checking Disabled) bit - (RFC4035, Section 3.2.2).+ } deriving (Eq, Show)+++-- | Default 'DNSFlags' record suitable for making recursive queries. By default+-- the RD bit is set, and the AD and CD bits are cleared.+--+defaultDNSFlags :: DNSFlags+defaultDNSFlags = DNSFlags+ { qOrR = QR_Query+ , opcode = OP_STD+ , authAnswer = False+ , trunCation = False+ , recDesired = True+ , recAvailable = False+ , authenData = False+ , chkDisable = False+ , rcode = NoErr+ }++----------------------------------------------------------------++-- | Boolean flag operations. These form a 'Monoid'. When combined via+-- `mappend`, as with function composition, the left-most value has+-- the last say.+--+-- >>> mempty :: FlagOp+-- FlagKeep+-- >>> FlagSet <> mempty+-- FlagSet+-- >>> FlagClear <> FlagSet <> mempty+-- FlagClear+-- >>> FlagReset <> FlagClear <> FlagSet <> mempty+-- FlagReset+data FlagOp = FlagSet -- ^ Set the flag to 1+ | FlagClear -- ^ Clear the flag to 0+ | FlagReset -- ^ Reset the flag to its default value+ | FlagKeep -- ^ Leave the flag unchanged+ deriving (Eq, Show)++-- $+-- Test associativity of the semigroup operation:+--+-- >>> let ops = [FlagSet, FlagClear, FlagReset, FlagKeep]+-- >>> foldl (&&) True [(a<>b)<>c == a<>(b<>c) | a <- ops, b <- ops, c <- ops]+-- True+--+instance Sem.Semigroup FlagOp where+ FlagKeep <> op = op+ op <> _ = op++instance Monoid FlagOp where+ mempty = FlagKeep+#if !(MIN_VERSION_base(4,11,0))+ -- this is redundant starting with base-4.11 / GHC 8.4+ -- if you want to avoid CPP, you can define `mappend = (<>)` unconditionally+ mappend = (Sem.<>)+#endif++-- | We don't show options left at their default value.+--+_skipDefault :: String+_skipDefault = ""++-- | Show non-default flag values+--+_showFlag :: String -> FlagOp -> String+_showFlag nm FlagSet = nm ++ ":1"+_showFlag nm FlagClear = nm ++ ":0"+_showFlag _ FlagReset = _skipDefault+_showFlag _ FlagKeep = _skipDefault++-- | Combine a list of options for display, skipping default values+--+_showOpts :: [String] -> String+_showOpts os = intercalate "," $ filter (/= _skipDefault) os++----------------------------------------------------------------++-- | Control over query-related DNS header flags. As with function composition,+-- the left-most value has the last say.+--+data HeaderControls = HeaderControls+ { rdBit :: !FlagOp+ , adBit :: !FlagOp+ , cdBit :: !FlagOp+ }+ deriving (Eq)++instance Sem.Semigroup HeaderControls where+ (HeaderControls rd1 ad1 cd1) <> (HeaderControls rd2 ad2 cd2) =+ HeaderControls (rd1 <> rd2) (ad1 <> ad2) (cd1 <> cd2)++instance Monoid HeaderControls where+ mempty = HeaderControls FlagKeep FlagKeep FlagKeep+#if !(MIN_VERSION_base(4,11,0))+ -- this is redundant starting with base-4.11 / GHC 8.4+ -- if you want to avoid CPP, you can define `mappend = (<>)` unconditionally+ mappend = (Sem.<>)+#endif++instance Show HeaderControls where+ show (HeaderControls rd ad cd) =+ _showOpts+ [ _showFlag "rd" rd+ , _showFlag "ad" ad+ , _showFlag "cd" cd ]++----------------------------------------------------------------++-- | The default EDNS Option list is empty. We define two operations, one to+-- prepend a list of options, and another to set a specific list of options.+--+data ODataOp = ODataAdd [OData] -- ^ Add the specified options to the list.+ | ODataSet [OData] -- ^ Set the option list as specified.+ deriving (Eq)++-- | Since any given option code can appear at most once in the list, we+-- de-duplicate by the OPTION CODE when combining lists.+--+_odataDedup :: ODataOp -> [OData]+_odataDedup op =+ nubBy ((==) `on` _odataToOptCode) $+ case op of+ ODataAdd os -> os+ ODataSet os -> os++-- $+-- Test associativity of the OData semigroup operation:+--+-- >>> let ip1 = IPv4 $ read "127.0.0.0"+-- >>> let ip2 = IPv4 $ read "192.0.2.0"+-- >>> let cs1 = OD_ClientSubnet 8 0 ip1+-- >>> let cs2 = OD_ClientSubnet 24 0 ip2+-- >>> let cs3 = OD_ECSgeneric 0 24 0 "foo"+-- >>> let dau1 = OD_DAU [3,5,7,8]+-- >>> let dau2 = OD_DAU [13,14]+-- >>> let dhu1 = OD_DHU [1,2]+-- >>> let dhu2 = OD_DHU [3,4]+-- >>> let nsid = OD_NSID ""+-- >>> let ops1 = [ODataAdd [dau1, dau2, cs1], ODataAdd [dau2, cs2, dhu1]]+-- >>> let ops2 = [ODataSet [], ODataSet [dhu2, cs3], ODataSet [nsid]]+-- >>> let ops = ops1 ++ ops2+-- >>> foldl (&&) True [(a<>b)<>c == a<>(b<>c) | a <- ops, b <- ops, c <- ops]+-- True++instance Sem.Semigroup ODataOp where+ ODataAdd as <> ODataAdd bs = ODataAdd $ as ++ bs+ ODataAdd as <> ODataSet bs = ODataSet $ as ++ bs+ ODataSet as <> _ = ODataSet as++instance Monoid ODataOp where+ mempty = ODataAdd []+#if !(MIN_VERSION_base(4,11,0))+ -- this is redundant starting with base-4.11 / GHC 8.4+ -- if you want to avoid CPP, you can define `mappend = (<>)` unconditionally+ mappend = (Sem.<>)+#endif++----------------------------------------------------------------++-- | EDNS query controls. When EDNS is disabled via @ednsEnabled FlagClear@,+-- all the other EDNS-related overrides have no effect.+--+-- >>> ednsHeader $ makeEmptyQuery $ ednsEnabled FlagClear <> doFlag FlagSet+-- NoEDNS+data EdnsControls = EdnsControls+ { extEn :: !FlagOp -- ^ Enabled+ , extVn :: !(Maybe Word8) -- ^ Version+ , extSz :: !(Maybe Word16) -- ^ UDP Size+ , extDO :: !FlagOp -- ^ DNSSEC OK (DO) bit+ , extOd :: !ODataOp -- ^ EDNS option list tweaks+ }+ deriving (Eq)++-- | Apply all the query flag overrides to 'defaultDNSFlags', returning the++instance Sem.Semigroup EdnsControls where+ (EdnsControls en1 vn1 sz1 do1 od1) <> (EdnsControls en2 vn2 sz2 do2 od2) =+ EdnsControls (en1 <> en2) (vn1 <|> vn2) (sz1 <|> sz2)+ (do1 <> do2) (od1 <> od2)++instance Monoid EdnsControls where+ mempty = EdnsControls FlagKeep Nothing Nothing FlagKeep mempty+#if !(MIN_VERSION_base(4,11,0))+ -- this is redundant starting with base-4.11 / GHC 8.4+ -- if you want to avoid CPP, you can define `mappend = (<>)` unconditionally+ mappend = (Sem.<>)+#endif++instance Show EdnsControls where+ show (EdnsControls en vn sz d0 od) =+ _showOpts+ [ _showFlag "edns.enabled" en+ , _showWord "edns.version" vn+ , _showWord "edns.udpsize" sz+ , _showFlag "edns.dobit" d0+ , _showOdOp "edns.options" $ map (show. _odataToOptCode)+ $ _odataDedup od ]+ where+ _showWord :: Show a => String -> Maybe a -> String+ _showWord nm w = maybe _skipDefault (\s -> nm ++ ":" ++ show s) w++ _showOdOp :: String -> [String] -> String+ _showOdOp nm os = case os of+ [] -> ""+ _ -> nm ++ ":[" ++ intercalate "," os ++ "]"++----------------------------------------------------------------++-- | Query controls form a 'Monoid', as with function composition, the+-- left-most value has the last say. The 'Monoid' is generated by two sets of+-- combinators, one that controls query-related DNS header flags, and another+-- that controls EDNS features.+--+-- The header flag controls are: 'rdFlag', 'adFlag' and 'cdFlag'.+--+-- The EDNS feature controls are: 'doFlag', 'ednsEnabled', 'ednsSetVersion',+-- 'ednsSetUdpSize' and 'ednsSetOptions'. When EDNS is disabled, all the other+-- EDNS-related controls have no effect.+--+-- __Example:__ Disable DNSSEC checking on the server, and request signatures and+-- NSEC records, perhaps for your own independent validation. The UDP buffer+-- size is set large, for use with a local loopback nameserver on the same host.+--+-- >>> :{+-- mconcat [ adFlag FlagClear+-- , cdFlag FlagSet+-- , doFlag FlagSet+-- , ednsSetUdpSize (Just 8192) -- IPv4 loopback server?+-- ]+-- :}+-- ad:0,cd:1,edns.udpsize:8192,edns.dobit:1+--+-- __Example:__ Use EDNS version 1 (yet to be specified), request nameserver+-- ids from the server, and indicate a client subnet of "192.0.2.1/24".+--+-- >>> :set -XOverloadedStrings+-- >>> let emptyNSID = ""+-- >>> let mask = 24+-- >>> let ipaddr = read "192.0.2.1"+-- >>> :{+-- mconcat [ ednsSetVersion (Just 1)+-- , ednsSetOptions (ODataAdd [OD_NSID emptyNSID])+-- , ednsSetOptions (ODataAdd [OD_ClientSubnet mask 0 ipaddr])+-- ]+-- :}+-- edns.version:1,edns.options:[NSID,ClientSubnet]++data QueryControls = QueryControls+ { qctlHeader :: !HeaderControls+ , qctlEdns :: !EdnsControls+ }+ deriving (Eq)++instance Sem.Semigroup QueryControls where+ (QueryControls fl1 ex1) <> (QueryControls fl2 ex2) =+ QueryControls (fl1 <> fl2) (ex1 <> ex2)++instance Monoid QueryControls where+ mempty = QueryControls mempty mempty+#if !(MIN_VERSION_base(4,11,0))+ -- this is redundant starting with base-4.11 / GHC 8.4+ -- if you want to avoid CPP, you can define `mappend = (<>)` unconditionally+ mappend = (Sem.<>)+#endif++instance Show QueryControls where+ show (QueryControls fl ex) = _showOpts [ show fl, show ex ]++----------------------------------------------------------------++-- | Generator of 'QueryControls' that adjusts the RD bit.+--+-- >>> rdFlag FlagClear+-- rd:0+rdFlag :: FlagOp -> QueryControls+rdFlag rd = mempty { qctlHeader = mempty { rdBit = rd } }++-- | Generator of 'QueryControls' that adjusts the AD bit.+--+-- >>> adFlag FlagSet+-- ad:1+adFlag :: FlagOp -> QueryControls+adFlag ad = mempty { qctlHeader = mempty { adBit = ad } }++-- | Generator of 'QueryControls' that adjusts the CD bit.+--+-- >>> cdFlag FlagSet+-- cd:1+cdFlag :: FlagOp -> QueryControls+cdFlag cd = mempty { qctlHeader = mempty { cdBit = cd } }++-- | Generator of 'QueryControls' that enables or disables EDNS support.+-- When EDNS is disabled, the rest of the 'EDNS' controls are ignored.+--+-- >>> ednsHeader $ makeEmptyQuery $ ednsEnabled FlagClear <> doFlag FlagSet+-- NoEDNS+ednsEnabled :: FlagOp -> QueryControls+ednsEnabled en = mempty { qctlEdns = mempty { extEn = en } }++-- | Generator of 'QueryControls' that adjusts the 'EDNS' version.+-- A value of 'Nothing' makes no changes, while 'Just' @v@ sets+-- the EDNS version to @v@.+--+-- >>> ednsSetVersion (Just 1)+-- edns.version:1+ednsSetVersion :: Maybe Word8 -> QueryControls+ednsSetVersion vn = mempty { qctlEdns = mempty { extVn = vn } }++-- | Generator of 'QueryControls' that adjusts the 'EDNS' UDP buffer size.+-- A value of 'Nothing' makes no changes, while 'Just' @n@ sets the EDNS UDP+-- buffer size to @n@.+--+-- >>> ednsSetUdpSize (Just 2048)+-- edns.udpsize:2048+ednsSetUdpSize :: Maybe Word16 -> QueryControls+ednsSetUdpSize sz = mempty { qctlEdns = mempty { extSz = sz } }++-- | Generator of 'QueryControls' that adjusts the 'EDNS' DnssecOk (DO) bit.+--+-- >>> doFlag FlagSet+-- edns.dobit:1+doFlag :: FlagOp -> QueryControls+doFlag d0 = mempty { qctlEdns = mempty { extDO = d0 } }++-- | Generator of 'QueryControls' that adjusts the list of 'EDNS' options.+--+-- >>> :set -XOverloadedStrings+-- >>> ednsSetOptions (ODataAdd [OD_NSID ""])+-- edns.options:[NSID]+ednsSetOptions :: ODataOp -> QueryControls+ednsSetOptions od = mempty { qctlEdns = mempty { extOd = od } }++----------------------------------------------------------------++-- | Query or response.+data QorR = QR_Query -- ^ Query.+ | QR_Response -- ^ Response.+ deriving (Eq, Show, Enum, Bounded)++-- | Kind of query.+data OPCODE+ = OP_STD -- ^ A standard query.+ | OP_INV -- ^ An inverse query (inverse queries are deprecated).+ | OP_SSR -- ^ A server status request.+ | OP_NOTIFY -- ^ A zone change notification (RFC1996)+ | OP_UPDATE -- ^ An update request (RFC2136)+ deriving (Eq, Show, Enum, Bounded)++-- | Convert a 16-bit DNS OPCODE number to its internal representation+--+toOPCODE :: Word16 -> Maybe OPCODE+toOPCODE i = case i of+ 0 -> Just OP_STD+ 1 -> Just OP_INV+ 2 -> Just OP_SSR+ -- OPCODE 3 is unassigned+ 4 -> Just OP_NOTIFY+ 5 -> Just OP_UPDATE+ _ -> Nothing++-- | Convert the internal representation of a DNS OPCODE to its 16-bit numeric+-- value.+--+fromOPCODE :: OPCODE -> Word16+fromOPCODE OP_STD = 0+fromOPCODE OP_INV = 1+fromOPCODE OP_SSR = 2+fromOPCODE OP_NOTIFY = 4+fromOPCODE OP_UPDATE = 5++----------------------------------------------------------------++-- | EDNS extended 12-bit response code. Non-EDNS messages use only the low 4+-- bits. With EDNS this stores the combined error code from the DNS header and+-- and the EDNS psuedo-header. See 'EDNSheader' for more detail.+newtype RCODE = RCODE {+ -- | Convert an 'RCODE' to its numeric value.+ fromRCODE :: Word16+ } deriving (Eq)++-- | Provide an Enum instance for backwards compatibility+instance Enum RCODE where+ fromEnum = fromIntegral . fromRCODE+ toEnum = RCODE . fromIntegral++-- | No error condition.+pattern NoErr :: RCODE+pattern NoErr = RCODE 0+-- | Format error - The name server was+-- unable to interpret the query.+pattern FormatErr :: RCODE+pattern FormatErr = RCODE 1+-- | Server failure - The name server was+-- unable to process this query due to a+-- problem with the name server.+pattern ServFail :: RCODE+pattern ServFail = RCODE 2+-- | Name Error - Meaningful only for+-- responses from an authoritative name+-- server, this code signifies that the+-- domain name referenced in the query does+-- not exist.+pattern NameErr :: RCODE+pattern NameErr = RCODE 3+-- | Not Implemented - The name server does+-- not support the requested kind of query.+pattern NotImpl :: RCODE+pattern NotImpl = RCODE 4+-- | Refused - The name server refuses to+-- perform the specified operation for+-- policy reasons. For example, a name+-- server may not wish to provide the+-- information to the particular requester,+-- or a name server may not wish to perform+-- a particular operation (e.g., zone+-- transfer) for particular data.+pattern Refused :: RCODE+pattern Refused = RCODE 5+-- | YXDomain - Dynamic update response, a pre-requisite domain that should not+-- exist, does exist.+pattern YXDomain :: RCODE+pattern YXDomain = RCODE 6+-- | YXRRSet - Dynamic update response, a pre-requisite RRSet that should not+-- exist, does exist.+pattern YXRRSet :: RCODE+pattern YXRRSet = RCODE 7+-- | NXRRSet - Dynamic update response, a pre-requisite RRSet that should+-- exist, does not exist.+pattern NXRRSet :: RCODE+pattern NXRRSet = RCODE 8+-- | NotAuth - Dynamic update response, the server is not authoritative for the+-- zone named in the Zone Section.+pattern NotAuth :: RCODE+pattern NotAuth = RCODE 9+-- | NotZone - Dynamic update response, a name used in the Prerequisite or+-- Update Section is not within the zone denoted by the Zone Section.+pattern NotZone :: RCODE+pattern NotZone = RCODE 10+-- | Bad OPT Version (BADVERS, RFC 6891).+pattern BadVers :: RCODE+pattern BadVers = RCODE 16+-- | Key not recognized [RFC2845]+pattern BadKey :: RCODE+pattern BadKey = RCODE 17+-- | Signature out of time window [RFC2845]+pattern BadTime :: RCODE+pattern BadTime = RCODE 18+-- | Bad TKEY Mode [RFC2930]+pattern BadMode :: RCODE+pattern BadMode = RCODE 19+-- | Duplicate key name [RFC2930]+pattern BadName :: RCODE+pattern BadName = RCODE 20+-- | Algorithm not supported [RFC2930]+pattern BadAlg :: RCODE+pattern BadAlg = RCODE 21+-- | Bad Truncation [RFC4635]+pattern BadTrunc :: RCODE+pattern BadTrunc = RCODE 22+-- | Bad/missing Server Cookie [RFC7873]+pattern BadCookie :: RCODE+pattern BadCookie = RCODE 23+-- | Malformed (peer) EDNS message, no RCODE available. This is not an RCODE+-- that can be sent by a peer. It lies outside the 12-bit range expressible+-- via EDNS. The low 12-bits are chosen to coincide with 'FormatErr'. When+-- an EDNS message is malformed, and we're unable to extract the extended RCODE,+-- the header 'rcode' is set to 'BadRCODE'.+pattern BadRCODE :: RCODE+pattern BadRCODE = RCODE 0x1001++-- | Use https://tools.ietf.org/html/rfc2929#section-2.3 names for DNS RCODEs+instance Show RCODE where+ show NoErr = "NoError"+ show FormatErr = "FormErr"+ show ServFail = "ServFail"+ show NameErr = "NXDomain"+ show NotImpl = "NotImp"+ show Refused = "Refused"+ show YXDomain = "YXDomain"+ show YXRRSet = "YXRRSet"+ show NotAuth = "NotAuth"+ show NotZone = "NotZone"+ show BadVers = "BadVers"+ show BadKey = "BadKey"+ show BadTime = "BadTime"+ show BadMode = "BadMode"+ show BadName = "BadName"+ show BadAlg = "BadAlg"+ show BadTrunc = "BadTrunc"+ show BadCookie = "BadCookie"+ show x = "RCODE " ++ (show $ fromRCODE x)++-- | Convert a numeric value to a corresponding 'RCODE'. The behaviour is+-- undefined for values outside the range @[0 .. 0xFFF]@ since the EDNS+-- extended RCODE is a 12-bit value. Values in the range @[0xF01 .. 0xFFF]@+-- are reserved for private use.+toRCODE :: Word16 -> RCODE+toRCODE = RCODE++----------------------------------------------------------------++-- XXX: The Question really should also include the CLASS+--+-- | Raw data format for DNS questions.+data Question = Question {+ qname :: Domain -- ^ A domain name+ , qtype :: TYPE -- ^ The type of the query+ } deriving (Eq, Show)++----------------------------------------------------------------++-- | Resource record class.+type CLASS = Word16++-- | Resource record class for the Internet.+classIN :: CLASS+classIN = 1++-- | Time to live in second.+type TTL = Word32++-- | Raw data format for resource records.+data ResourceRecord = ResourceRecord {+ rrname :: !Domain -- ^ Name+ , rrtype :: !TYPE -- ^ Resource record type+ , rrclass :: !CLASS -- ^ Resource record class+ , rrttl :: !TTL -- ^ Time to live+ , rdata :: !RData -- ^ Resource data+ } deriving (Eq,Show)++----------------------------------------------------------------++-- | Given a 32-bit circle-arithmetic DNS time, and the current absolute epoch+-- time, return the epoch time corresponding to the DNS timestamp.+--+dnsTime :: Word32 -- ^ DNS circle-arithmetic timestamp+ -> Int64 -- ^ current epoch time+ -> Int64 -- ^ absolute DNS timestamp+dnsTime tdns tnow =+ let delta = tdns - fromIntegral tnow+ in if delta > 0x7FFFFFFF -- tdns is in the past?+ then tnow - (0x100000000 - fromIntegral delta)+ else tnow + fromIntegral delta++-- | RRSIG representation.+--+-- As noted in+-- <https://tools.ietf.org/html/rfc4034#section-3.1.5 Section 3.1.5 of RFC 4034>+-- the RRsig inception and expiration times use serial number arithmetic. As a+-- result these timestamps /are not/ pure values, their meaning is+-- time-dependent! They depend on the present time and are both at most+-- approximately +\/-68 years from the present. This ambiguity is not a+-- problem because cached RRSIG records should only persist a few days,+-- signature lifetimes should be *much* shorter than 68 years, and key rotation+-- should result any misconstrued 136-year-old signatures fail to validate.+-- This also means that the interpretation of a time that is exactly half-way+-- around the clock at @now +\/-0x80000000@ is not important, the signature+-- should never be valid.+--+-- The upshot for us is that we need to convert these *impure* relative values+-- to pure absolute values at the moment they are received from from the network+-- (or read from files, ... in some impure I/O context), and convert them back to+-- 32-bit values when encoding. Therefore, the constructor takes absolute+-- 64-bit representations of the inception and expiration times.+--+-- The 'dnsTime' function performs the requisite conversion.+--+data RD_RRSIG = RDREP_RRSIG+ { rrsigType :: !TYPE -- ^ RRtype of RRset signed+ , rrsigKeyAlg :: !Word8 -- ^ DNSKEY algorithm+ , rrsigNumLabels :: !Word8 -- ^ Number of labels signed+ , rrsigTTL :: !Word32 -- ^ Maximum origin TTL+ , rrsigExpiration :: !Int64 -- ^ Time last valid+ , rrsigInception :: !Int64 -- ^ Time first valid+ , rrsigKeyTag :: !Word16 -- ^ Signing key tag+ , rrsigZone :: !Domain -- ^ Signing domain+ , rrsigValue :: !ByteString -- ^ Opaque signature+ }+ deriving (Eq, Ord)++instance Show RD_RRSIG where+ show RDREP_RRSIG{..} = unwords+ [ show rrsigType+ , show rrsigKeyAlg+ , show rrsigNumLabels+ , show rrsigTTL+ , showTime rrsigExpiration+ , showTime rrsigInception+ , show rrsigKeyTag+ , BS.unpack rrsigZone+ , _b64encode rrsigValue+ ]+ where+ showTime :: Int64 -> String+ showTime t = H.timePrint fmt $ H.Elapsed $ H.Seconds t+ where+ fmt = [ H.Format_Year4, H.Format_Month2, H.Format_Day2+ , H.Format_Hour, H.Format_Minute, H.Format_Second ]++-- | Raw data format for each type.+data RData = RD_A IPv4 -- ^ IPv4 address+ | RD_NS Domain -- ^ An authoritative name serve+ | RD_CNAME Domain -- ^ The canonical name for an alias+ | RD_SOA Domain Mailbox Word32 Word32 Word32 Word32 Word32+ -- ^ Marks the start of a zone of authority+ | RD_NULL ByteString -- ^ NULL RR (EXPERIMENTAL, RFC1035).+ | RD_PTR Domain -- ^ A domain name pointer+ | RD_MX Word16 Domain -- ^ Mail exchange+ | RD_TXT ByteString -- ^ Text strings+ | RD_RP Mailbox Domain -- ^ Responsible Person (RFC1183)+ | RD_AAAA IPv6 -- ^ IPv6 Address+ | RD_SRV Word16 Word16 Word16 Domain+ -- ^ Server Selection (RFC2782)+ | RD_DNAME Domain -- ^ DNAME (RFC6672)+ | RD_OPT [OData] -- ^ OPT (RFC6891)+ | RD_DS Word16 Word8 Word8 ByteString -- ^ Delegation Signer (RFC4034)+ | RD_RRSIG RD_RRSIG -- ^ DNSSEC signature+ | RD_NSEC Domain [TYPE] -- ^ DNSSEC denial of existence NSEC record+ | RD_DNSKEY Word16 Word8 Word8 ByteString+ -- ^ DNSKEY (RFC4034)+ | RD_NSEC3 Word8 Word8 Word16 ByteString ByteString [TYPE]+ -- ^ DNSSEC hashed denial of existence (RFC5155)+ | RD_NSEC3PARAM Word8 Word8 Word16 ByteString+ -- ^ NSEC3 zone parameters (RFC5155)+ | RD_TLSA Word8 Word8 Word8 ByteString+ -- ^ TLSA (RFC6698)+ | RD_CDS Word16 Word8 Word8 ByteString+ -- ^ Child DS (RFC7344)+ | RD_CDNSKEY Word16 Word8 Word8 ByteString+ -- ^ Child DNSKEY (RFC7344)+ | RD_CAA Word8 (CI ByteString) ByteString+ -- ^ CAA (RFC 6844)+ --RD_CSYNC+ | UnknownRData ByteString -- ^ Unknown resource data+ deriving (Eq, Ord)++instance Show RData where+ show rd = case rd of+ RD_A address -> show address+ RD_NS nsdname -> showDomain nsdname+ RD_CNAME cname -> showDomain cname+ RD_SOA a b c d e f g -> showSOA a b c d e f g+ RD_NULL bytes -> showOpaque bytes+ RD_PTR ptrdname -> showDomain ptrdname+ RD_MX pref exch -> showMX pref exch+ RD_TXT textstring -> showTXT textstring+ RD_RP mbox dname -> showRP mbox dname+ RD_AAAA address -> show address+ RD_SRV pri wei prt tgt -> showSRV pri wei prt tgt+ RD_DNAME target -> showDomain target+ RD_OPT options -> show options+ RD_DS tag alg dalg d -> showDS tag alg dalg d+ RD_RRSIG rrsig -> show rrsig+ RD_NSEC next types -> showNSEC next types+ RD_DNSKEY f p a k -> showDNSKEY f p a k+ RD_NSEC3 a f i s h types -> showNSEC3 a f i s h types+ RD_NSEC3PARAM a f i s -> showNSEC3PARAM a f i s+ RD_TLSA u s m d -> showTLSA u s m d+ RD_CDS tag alg dalg d -> showDS tag alg dalg d+ RD_CDNSKEY f p a k -> showDNSKEY f p a k+ RD_CAA f t v -> showCAA f t v+ UnknownRData bytes -> showOpaque bytes+ where+ showSalt "" = "-"+ showSalt salt = _b16encode salt+ showDomain = BS.unpack+ showSOA mname rname serial refresh retry expire minttl =+ showDomain mname ++ " " ++ showDomain rname ++ " " +++ show serial ++ " " ++ show refresh ++ " " +++ show retry ++ " " ++ show expire ++ " " ++ show minttl+ showMX preference exchange =+ show preference ++ " " ++ showDomain exchange+ showTXT bs = '"' : B.foldr dnsesc ['"'] bs+ where+ c2w = fromIntegral . fromEnum+ w2c = toEnum . fromIntegral+ doubleQuote = c2w '"'+ backSlash = c2w '\\'+ dnsesc c s+ | c == doubleQuote = '\\' : w2c c : s+ | c == backSlash = '\\' : w2c c : s+ | c >= 32 && c < 127 = w2c c : s+ | otherwise = '\\' : ddd c s+ ddd c s =+ let (q100, r100) = divMod (fromIntegral c) 100+ (q10, r10) = divMod r100 10+ in intToDigit q100 : intToDigit q10 : intToDigit r10 : s+ showRP mbox dname = showDomain mbox ++ " " ++ showDomain dname+ showSRV priority weight port target =+ show priority ++ " " ++ show weight ++ " " +++ show port ++ " " ++ BS.unpack target+ showDS keytag alg digestType digest =+ show keytag ++ " " ++ show alg ++ " " +++ show digestType ++ " " ++ _b16encode digest+ showNSEC next types =+ unwords $ showDomain next : map show types+ showDNSKEY flags protocol alg key =+ show flags ++ " " ++ show protocol ++ " " +++ show alg ++ " " ++ _b64encode key+ -- | <https://tools.ietf.org/html/rfc5155#section-3.2>+ showNSEC3 hashalg flags iterations salt nexthash types =+ unwords $ show hashalg : show flags : show iterations :+ showSalt salt : _b32encode nexthash : map show types+ showNSEC3PARAM hashAlg flags iterations salt =+ show hashAlg ++ " " ++ show flags ++ " " +++ show iterations ++ " " ++ showSalt salt+ showTLSA usage selector mtype digest =+ show usage ++ " " ++ show selector ++ " " +++ show mtype ++ " " ++ _b16encode digest+ showCAA flags tag value =+ show flags ++ " " ++ BS.unpack (CI.original tag) ++ " " ++ show value+ -- | Opaque RData: <https://tools.ietf.org/html/rfc3597#section-5>+ showOpaque bs = unwords ["\\#", show (BS.length bs), _b16encode bs]++_b16encode, _b32encode, _b64encode :: ByteString -> String+_b16encode = BS.unpack. B16.encode+_b32encode = BS.unpack. B32.encode+_b64encode = BS.unpack. B64.encode++-- | Type alias for resource records in the answer section.+type Answers = [ResourceRecord]++-- | Type alias for resource records in the answer section.+type AuthorityRecords = [ResourceRecord]++-- | Type for resource records in the additional section.+type AdditionalRecords = [ResourceRecord]++----------------------------------------------------------------++-- | A 'DNSMessage' template for queries with default settings for+-- the message 'DNSHeader' and 'EDNSheader'. This is the initial+-- query message state, before customization via 'QueryControls'.+--+defaultQuery :: DNSMessage+defaultQuery = DNSMessage {+ header = DNSHeader {+ identifier = 0+ , flags = defaultDNSFlags+ }+ , ednsHeader = EDNSheader defaultEDNS+ , question = []+ , answer = []+ , authority = []+ , additional = []+ }++-- | Default response. When responding to EDNS queries, the response must+-- either be an EDNS response, or else FormatErr must be returned. The default+-- response message has EDNS disabled ('ednsHeader' set to 'NoEDNS'), it should+-- be updated as appropriate.+--+-- Do not explicitly add OPT RRs to the additional section, instead let the+-- encoder compute and add the OPT record based on the EDNS pseudo-header.+--+-- The 'RCODE' in the 'DNSHeader' should be set to the appropriate 12-bit+-- extended value, which will be split between the primary header and EDNS OPT+-- record during message encoding (low 4 bits in DNS header, high 8 bits in+-- EDNS OPT record). See 'EDNSheader' for more details.+--+defaultResponse :: DNSMessage+defaultResponse = DNSMessage {+ header = DNSHeader {+ identifier = 0+ , flags = defaultDNSFlags {+ qOrR = QR_Response+ , authAnswer = True+ , recAvailable = True+ , authenData = False+ }+ }+ , ednsHeader = NoEDNS+ , question = []+ , answer = []+ , authority = []+ , additional = []+ }++-- | A query template with 'QueryControls' overrides applied,+-- with just the 'Question' and query 'Identifier' remaining+-- to be filled in.+--+makeEmptyQuery :: QueryControls -- ^ Flag and EDNS overrides+ -> DNSMessage+makeEmptyQuery ctls = defaultQuery {+ header = header'+ , ednsHeader = queryEdns ehctls+ }+ where+ hctls = qctlHeader ctls+ ehctls = qctlEdns ctls+ header' = (header defaultQuery) { flags = queryDNSFlags hctls }++ -- | Apply the given 'FlagOp' to a default boolean value to produce the final+ -- setting.+ --+ applyFlag :: FlagOp -> Bool -> Bool+ applyFlag FlagSet _ = True+ applyFlag FlagClear _ = False+ applyFlag _ v = v++ -- | Construct a list of 0 or 1 EDNS OPT RRs based on EdnsControls setting.+ --+ queryEdns :: EdnsControls -> EDNSheader+ queryEdns (EdnsControls en vn sz d0 od) =+ let d = defaultEDNS+ in if en == FlagClear+ then NoEDNS+ else EDNSheader $ d { ednsVersion = fromMaybe (ednsVersion d) vn+ , ednsUdpSize = fromMaybe (ednsUdpSize d) sz+ , ednsDnssecOk = applyFlag d0 (ednsDnssecOk d)+ , ednsOptions = _odataDedup od+ }++ -- | Apply all the query flag overrides to 'defaultDNSFlags', returning the+ -- resulting 'DNSFlags' suitable for making queries with the requested flag+ -- settings. This is only needed if you're creating your own 'DNSMessage',+ -- the 'Network.DNS.LookupRaw.lookupRawCtl' function takes a 'QueryControls'+ -- argument and handles this conversion internally.+ --+ -- Default overrides can be specified in the resolver configuration by setting+ -- the 'Network.DNS.resolvQueryControls' field of the+ -- 'Network.DNS.Resolver.ResolvConf' argument to+ -- 'Network.DNS.Resolver.makeResolvSeed'. These then apply to lookups via+ -- resolvers based on the resulting configuration, with the exception of+ -- 'Network.DNS.LookupRaw.lookupRawCtl' which takes an additional+ -- 'QueryControls' argument to augment the default overrides.+ --+ queryDNSFlags :: HeaderControls -> DNSFlags+ queryDNSFlags (HeaderControls rd ad cd) = d {+ recDesired = applyFlag rd $ recDesired d+ , authenData = applyFlag ad $ authenData d+ , chkDisable = applyFlag cd $ chkDisable d+ }+ where+ d = defaultDNSFlags++-- | Construct a complete query 'DNSMessage', by combining the 'defaultQuery'+-- template with the specified 'Identifier', and 'Question'. The+-- 'QueryControls' can be 'mempty' to leave all header and EDNS settings at+-- their default values, or some combination of overrides. A default set of+-- overrides can be enabled via the 'Network.DNS.Resolver.resolvQueryControls'+-- field of 'Network.DNS.Resolver.ResolvConf'. Per-query overrides are+-- possible by using 'Network.DNS.LookupRaw.loookupRawCtl'.+--+makeQuery :: Identifier -- ^ Crypto random request id+ -> Question -- ^ Question name and type+ -> QueryControls -- ^ Custom RD\/AD\/CD flags and EDNS settings+ -> DNSMessage+makeQuery idt q ctls = empqry {+ header = (header empqry) { identifier = idt }+ , question = [q]+ }+ where+ empqry = makeEmptyQuery ctls++-- | Construct a query response 'DNSMessage'.+makeResponse :: Identifier+ -> Question+ -> Answers+ -> DNSMessage+makeResponse idt q as = defaultResponse {+ header = header' { identifier = idt }+ , question = [q]+ , answer = as+ }+ where+ header' = header defaultResponse++----------------------------------------------------------------+-- EDNS (RFC 6891, EDNS(0))+----------------------------------------------------------------++-- | EDNS information defined in RFC 6891.+data EDNS = EDNS {+ -- | EDNS version, presently only version 0 is defined.+ ednsVersion :: !Word8+ -- | Supported UDP payload size.+ , ednsUdpSize :: !Word16+ -- | Request DNSSEC replies (with RRSIG and NSEC records as as appropriate)+ -- from the server. Generally, not needed (except for diagnostic purposes)+ -- unless the signatures will be validated. Just setting the 'AD' bit in+ -- the query and checking it in the response is sufficient (but often+ -- subject to man-in-the-middle forgery) if all that's wanted is whether+ -- the server validated the response.+ , ednsDnssecOk :: !Bool+ -- | EDNS options (e.g. 'OD_NSID', 'OD_ClientSubnet', ...)+ , ednsOptions :: ![OData]+ } deriving (Eq, Show)++-- | The default EDNS pseudo-header for queries. The UDP buffer size is set to+-- 1216 bytes, which should result in replies that fit into the 1280 byte+-- IPv6 minimum MTU. Since IPv6 only supports fragmentation at the source,+-- and even then not all gateways forward IPv6 pre-fragmented IPv6 packets,+-- it is best to keep DNS packet sizes below this limit when using IPv6+-- nameservers. A larger value may be practical when using IPv4 exclusively.+--+-- @+-- defaultEDNS = EDNS+-- { ednsVersion = 0 -- The default EDNS version is 0+-- , ednsUdpSize = 1232 -- IPv6-safe UDP MTU (RIPE recommendation)+-- , ednsDnssecOk = False -- We don't do DNSSEC validation+-- , ednsOptions = [] -- No EDNS options by default+-- }+-- @+--+defaultEDNS :: EDNS+defaultEDNS = EDNS+ { ednsVersion = 0 -- The default EDNS version is 0+ , ednsUdpSize = 1232 -- IPv6-safe UDP MTU+ , ednsDnssecOk = False -- We don't do DNSSEC validation+ , ednsOptions = [] -- No EDNS options by default+ }++-- | Maximum UDP size that can be advertised. If the 'ednsUdpSize' of 'EDNS'+-- is larger, then this value is sent instead. This value is likely to work+-- only for local nameservers on the loopback network. Servers may enforce+-- a smaller limit.+--+-- >>> maxUdpSize+-- 16384+maxUdpSize :: Word16+maxUdpSize = 16384++-- | Minimum UDP size to advertise. If 'ednsUdpSize' of 'EDNS' is smaller,+-- then this value is sent instead.+--+-- >>> minUdpSize+-- 512+minUdpSize :: Word16+minUdpSize = 512++----------------------------------------------------------------++-- | EDNS Option Code (RFC 6891).+newtype OptCode = OptCode {+ -- | From option code to number.+ fromOptCode :: Word16+ } deriving (Eq,Ord)++-- | NSID (RFC5001, section 2.3)+pattern NSID :: OptCode+pattern NSID = OptCode 3++-- | DNSSEC algorithm support (RFC6974, section 3)+pattern DAU :: OptCode+pattern DAU = OptCode 5+pattern DHU :: OptCode+pattern DHU = OptCode 6+pattern N3U :: OptCode+pattern N3U = OptCode 7++-- | Client subnet (RFC7871)+pattern ClientSubnet :: OptCode+pattern ClientSubnet = OptCode 8++instance Show OptCode where+ show NSID = "NSID"+ show DAU = "DAU"+ show DHU = "DHU"+ show N3U = "N3U"+ show ClientSubnet = "ClientSubnet"+ show x = "OptCode" ++ (show $ fromOptCode x)++-- | From number to option code.+toOptCode :: Word16 -> OptCode+toOptCode = OptCode++----------------------------------------------------------------++-- | RData formats for a few EDNS options, and an opaque catchall+data OData =+ -- | Name Server Identifier (RFC5001). Bidirectional, empty from client.+ -- (opaque octet-string). May contain binary data, which MUST be empty+ -- in queries.+ OD_NSID ByteString+ -- | DNSSEC Algorithm Understood (RFC6975). Client to server.+ -- (array of 8-bit numbers). Lists supported DNSKEY algorithms.+ | OD_DAU [Word8]+ -- | DS Hash Understood (RFC6975). Client to server.+ -- (array of 8-bit numbers). Lists supported DS hash algorithms.+ | OD_DHU [Word8]+ -- | NSEC3 Hash Understood (RFC6975). Client to server.+ -- (array of 8-bit numbers). Lists supported NSEC3 hash algorithms.+ | OD_N3U [Word8]+ -- | Client subnet (RFC7871). Bidirectional.+ -- (source bits, scope bits, address).+ -- The address is masked and truncated when encoding queries. The+ -- address is zero-padded when decoding. Invalid input encodings+ -- result in an 'OD_ECSgeneric' value instead.+ --+ | OD_ClientSubnet Word8 Word8 IP+ -- | Unsupported or malformed IP client subnet option. Bidirectional.+ -- (address family, source bits, scope bits, opaque address).+ | OD_ECSgeneric Word16 Word8 Word8 ByteString+ -- | Generic EDNS option.+ -- (numeric 'OptCode', opaque content)+ | UnknownOData Word16 ByteString+ deriving (Eq,Ord)+++-- | Recover the (often implicit) 'OptCode' from a value of the 'OData' sum+-- type.+_odataToOptCode :: OData -> OptCode+_odataToOptCode OD_NSID {} = NSID+_odataToOptCode OD_DAU {} = DAU+_odataToOptCode OD_DHU {} = DHU+_odataToOptCode OD_N3U {} = N3U+_odataToOptCode OD_ClientSubnet {} = ClientSubnet+_odataToOptCode OD_ECSgeneric {} = ClientSubnet+_odataToOptCode (UnknownOData code _) = toOptCode code++instance Show OData where+ show (OD_NSID nsid) = _showNSID nsid+ show (OD_DAU as) = _showAlgList "DAU" as+ show (OD_DHU hs) = _showAlgList "DHU" hs+ show (OD_N3U hs) = _showAlgList "N3U" hs+ show (OD_ClientSubnet b1 b2 ip@(IPv4 _)) = _showECS 1 b1 b2 $ show ip+ show (OD_ClientSubnet b1 b2 ip@(IPv6 _)) = _showECS 2 b1 b2 $ show ip+ show (OD_ECSgeneric fam b1 b2 a) = _showECS fam b1 b2 $ _b16encode a+ show (UnknownOData code bs) =+ "UnknownOData " ++ show code ++ " " ++ _b16encode bs++_showAlgList :: String -> [Word8] -> String+_showAlgList nm ws = nm ++ " " ++ intercalate "," (map show ws)++_showNSID :: ByteString -> String+_showNSID nsid = "NSID" ++ " " ++ _b16encode nsid ++ ";" ++ printable nsid+ where+ printable = BS.unpack. BS.map (\c -> if c < ' ' || c > '~' then '?' else c)++_showECS :: Word16 -> Word8 -> Word8 -> String -> String+_showECS family srcBits scpBits address =+ show family ++ " " ++ show srcBits+ ++ " " ++ show scpBits ++ " " ++ address
+ internal/Network/DNS/Types/Resolver.hs view
@@ -0,0 +1,136 @@+module Network.DNS.Types.Resolver where++import Network.Socket (AddrInfo(..), PortNumber, HostName)++import Network.DNS.Imports+import Network.DNS.Memo+import Network.DNS.Types.Internal++----------------------------------------------------------------++-- | The type to specify a cache server.+data FileOrNumericHost = RCFilePath FilePath -- ^ A path for \"resolv.conf\"+ -- where one or more IP addresses+ -- of DNS servers should be found+ -- on Unix.+ -- Default DNS servers are+ -- automatically detected+ -- on Windows regardless of+ -- the value of the file name.+ | RCHostName HostName -- ^ A numeric IP address. /Warning/: host names are invalid.+ | RCHostNames [HostName] -- ^ Numeric IP addresses. /Warning/: host names are invalid.+ | RCHostPort HostName PortNumber -- ^ A numeric IP address and port number. /Warning/: host names are invalid.+ deriving Show++----------------------------------------------------------------++-- | Cache configuration for responses.+data CacheConf = CacheConf {+ -- | If RR's TTL is higher than this value, this value is used instead.+ maximumTTL :: TTL+ -- | Cache pruning interval in seconds.+ , pruningDelay :: Int+ } deriving Show++-- | Default cache configuration.+--+-- >>> defaultCacheConf+-- CacheConf {maximumTTL = 300, pruningDelay = 10}+defaultCacheConf :: CacheConf+defaultCacheConf = CacheConf 300 10++----------------------------------------------------------------++-- | Type for resolver configuration.+-- Use 'defaultResolvConf' to create a new value.+--+-- An example to use Google's public DNS cache instead of resolv.conf:+--+-- >>> let conf = defaultResolvConf { resolvInfo = RCHostName "8.8.8.8" }+--+-- An example to use multiple Google's public DNS cache concurrently:+--+-- >>> let conf = defaultResolvConf { resolvInfo = RCHostNames ["8.8.8.8","8.8.4.4"], resolvConcurrent = True }+--+-- An example to disable EDNS:+--+-- >>> let conf = defaultResolvConf { resolvQueryControls = ednsEnabled FlagClear }+--+-- An example to enable query result caching:+--+-- >>> let conf = defaultResolvConf { resolvCache = Just defaultCacheConf }+--+-- An example to disable requesting recursive service.+--+-- >>> let conf = defaultResolvConf { resolvQueryControls = rdFlag FlagClear }+--+-- An example to set the AD bit in all queries by default.+--+-- >>> let conf = defaultResolvConf { resolvQueryControls = adFlag FlagSet }+--+-- An example to set the both the AD and CD bits in all queries by default.+--+-- >>> let conf = defaultResolvConf { resolvQueryControls = adFlag FlagSet <> cdFlag FlagSet }+--+-- An example with an EDNS buffer size of 1216 bytes, which is more robust with+-- IPv6, and the DO bit set to request DNSSEC responses.+--+-- >>> let conf = defaultResolvConf { resolvQueryControls = ednsSetUdpSize (Just 1216) <> doFlag FlagSet }+--+data ResolvConf = ResolvConf {+ -- | Server information.+ resolvInfo :: FileOrNumericHost+ -- | Timeout in micro seconds.+ , resolvTimeout :: Int+ -- | The number of retries including the first try.+ , resolvRetry :: Int+ -- | Concurrent queries if multiple DNS servers are specified.+ , resolvConcurrent :: Bool+ -- | Cache configuration.+ , resolvCache :: Maybe CacheConf+ -- | Overrides for the default flags used for queries via resolvers that use+ -- this configuration.+ , resolvQueryControls :: QueryControls+} deriving Show++-- | Return a default 'ResolvConf':+--+-- * 'resolvInfo' is 'RCFilePath' \"\/etc\/resolv.conf\".+-- * 'resolvTimeout' is 3,000,000 micro seconds.+-- * 'resolvRetry' is 3.+-- * 'resolvConcurrent' is False.+-- * 'resolvCache' is Nothing.+-- * 'resolvQueryControls' is an empty set of overrides.+defaultResolvConf :: ResolvConf+defaultResolvConf = ResolvConf {+ resolvInfo = RCFilePath "/etc/resolv.conf"+ , resolvTimeout = 3 * 1000 * 1000+ , resolvRetry = 3+ , resolvConcurrent = False+ , resolvCache = Nothing+ , resolvQueryControls = mempty+}++----------------------------------------------------------------++-- | Intermediate abstract data type for resolvers.+-- IP address information of DNS servers is generated+-- according to 'resolvInfo' internally.+-- This value can be safely reused for 'withResolver'.+--+-- The naming is confusing for historical reasons.+data ResolvSeed = ResolvSeed {+ resolvconf :: ResolvConf+ , nameservers :: NonEmpty AddrInfo+}++----------------------------------------------------------------++-- | Abstract data type of DNS Resolver.+-- This includes newly seeded identifier generators for all+-- specified DNS servers and a cache database.+data Resolver = Resolver {+ resolvseed :: ResolvSeed+ , genIds :: NonEmpty (IO Word16)+ , cache :: Maybe Cache+}
+ test/DecodeSpec.hs view
@@ -0,0 +1,131 @@+{-# LANGUAGE OverloadedStrings #-}++module DecodeSpec where++import Data.ByteString.Internal (ByteString(..), unsafeCreate)+import qualified Data.ByteString.Char8 as BC+import Data.Word8+import Foreign.ForeignPtr (withForeignPtr)+import Foreign.Ptr (plusPtr)+import Foreign.Storable (peek, poke, peekByteOff)+import Test.Hspec++import Network.DNS+import Network.DNS.Imports++----------------------------------------------------------------++test_doublePointer :: ByteString+test_doublePointer = "f7eb8500000100010007000404736563330561706e696303636f6d0000010001c00c0001000100001c200004ca0c1c8cc0110002000100001c20000f036e73310561706e6963036e657400c0300002000100001c200006036e7333c040c0300002000100001c200006036e7334c040c0300002000100001c20001004736563310561706e696303636f6d00c0300002000100001c20001704736563310761757468646e730472697065036e657400c0300002000100001c20001004736563320561706e696303636f6d00c0300002000100001c2000070473656333c0bfc07b0001000100001c200004ca0c1d3bc07b001c000100001c20001020010dc02001000a4608000000000059c0ba0001000100001c200004ca0c1d3cc0d6001c000100001c20001020010dc0000100004777000000000140"+-- DNSMessage {header = DNSHeader {identifier = 63467, flags = DNSFlags {qOrR = QR_Response, opcode = OP_STD, authAnswer = True, trunCation = False, recDesired = True, recAvailable = False, rcode = NoErr, authenData = False}}, question = [Question {qname = "sec3.apnic.com.", qtype = A}], answer = [ResourceRecord {rrname = "sec3.apnic.com.", rrtype = A, rrttl = 7200, rdata = 202.12.28.140}], authority = [ResourceRecord {rrname = "apnic.com.", rrtype = NS, rrttl = 7200, rdata = ns1.apnic.net.},ResourceRecord {rrname = "apnic.com.", rrtype = NS, rrttl = 7200, rdata = ns3.apnic.net.},ResourceRecord {rrname = "apnic.com.", rrtype = NS, rrttl = 7200, rdata = ns4.apnic.net.},ResourceRecord {rrname = "apnic.com.", rrtype = NS, rrttl = 7200, rdata = sec1.apnic.com.},ResourceRecord {rrname = "apnic.com.", rrtype = NS, rrttl = 7200, rdata = sec1.authdns.ripe.net.},ResourceRecord {rrname = "apnic.com.", rrtype = NS, rrttl = 7200, rdata = sec2.apnic.com.},ResourceRecord {rrname = "apnic.com.", rrtype = NS, rrttl = 7200, rdata = sec3.apnic.com.}], additional = [ResourceRecord {rrname = "sec1.apnic.com.", rrtype = A, rrttl = 7200, rdata = 202.12.29.59},ResourceRecord {rrname = "sec1.apnic.com.", rrtype = AAAA, rrttl = 7200, rdata = 2001:dc0:2001:a:4608::59},ResourceRecord {rrname = "sec2.apnic.com.", rrtype = A, rrttl = 7200, rdata = 202.12.29.60},ResourceRecord {rrname = "sec3.apnic.com.", rrtype = AAAA, rrttl = 7200, rdata = 2001:dc0:1:0:4777::140}]})++test_txt :: ByteString+test_txt = "463181800001000100000000076e69636f6c6173046b766462076e647072696d6102696f0000100001c00c0010000100000e10000d0c6e69636f6c61732e6b766462"+-- DNSMessage {header = DNSHeader {identifier = 17969, flags = DNSFlags {qOrR = QR_Response, opcode = OP_STD, authAnswer = False, trunCation = False, recDesired = True, recAvailable = True, rcode = NoErr, authenData = False}}+-- , question = [Question {qname = "nicolas.kvdb.ndprima.io.", qtype = TXT}]+-- , answer = [ResourceRecord {rrname = "nicolas.kvdb.ndprima.io.", rrtype = TXT, rrttl = 3600, rdata = nicolas.kvdb}]+-- , authority = []+-- , additional = []})++test_dname :: ByteString+test_dname = "b3c0818000010005000200010377777706376b616e616c02636f02696c0000010001c0100027000100000003000c0769737261656c3702727500c00c0005000100000003000603777777c02ec046000500010000255b0002c02ec02e000100010000003d000451daf938c02e000100010000003d0004c33ce84ac02e000200010005412b000c036e7332026137036f726700c02e000200010005412b0006036e7331c08a0000291000000000000000"+-- DNSMessage {header = DNSHeader {identifier = 46016, flags = DNSFlags {qOrR = QR_Response, opcode = OP_STD, authAnswer = False, trunCation = False, recDesired = True, recAvailable = True, rcode = NoErr, authenData = False}}, question = [Question {qname = "www.7kanal.co.il.", qtype = A}], answer = [ResourceRecord {rrname = "7kanal.co.il.", rrtype = DNAME, rrttl = 3, rdata = israel7.ru.},ResourceRecord {rrname = "www.7kanal.co.il.", rrtype = CNAME, rrttl = 3, rdata = www.israel7.ru.},ResourceRecord {rrname = "www.israel7.ru.", rrtype = CNAME, rrttl = 9563, rdata = israel7.ru.},ResourceRecord {rrname = "israel7.ru.", rrtype = A, rrttl = 61, rdata = 81.218.249.56},ResourceRecord {rrname = "israel7.ru.", rrtype = A, rrttl = 61, rdata = 195.60.232.74}], authority = [ResourceRecord {rrname = "israel7.ru.", rrtype = NS, rrttl = 344363, rdata = ns2.a7.org.},ResourceRecord {rrname = "israel7.ru.", rrtype = NS, rrttl = 344363, rdata = ns1.a7.org.}], additional = [OptRecord {orudpsize = 4096, ordnssecok = False, orversion = 0, rdata = []}]})++test_mx :: ByteString+test_mx = "f03681800001000100000001036d6577036f726700000f0001c00c000f000100000df10009000a046d61696cc00c0000291000000000000000"+-- DNSMessage {header = DNSHeader {identifier = 61494, flags = DNSFlags {qOrR = QR_Response, opcode = OP_STD, authAnswer = False, trunCation = False, recDesired = True, recAvailable = True, rcode = NoErr, authenData = False}}+-- , question = [Question {qname = "mew.org.", qtype = MX}]+-- , answer = [ResourceRecord {rrname = "mew.org.", rrtype = MX, rrttl = 3569, rdata = 10 mail.mew.org.}]+-- , authority = []+-- , additional = [OptRecord {orudpsize = 4096, ordnssecok = False, orversion = 0, rdata = []}]})++-- Message with question domain == SOA rname, testing correct decoding of+-- of the rname to presentation form when it encoded in compressed form+-- as a pointer to the question domain.+test_soa_in :: DNSMessage+test_soa_in =+ let soard = RD_SOA "ns1.example.com." "hostmaster.example.com." 0 0 0 0 0+ soarr = ResourceRecord "example.com." SOA 1 3600 soard+ in defaultResponse { question = [Question "hostmaster.example.com." A]+ , authority = [soarr] }++-- Expected decoded presentation form of the 'test_soa' message.+test_soa_out :: DNSMessage+test_soa_out =+ let soard = RD_SOA "ns1.example.com." "hostmaster@example.com." 0 0 0 0 0+ soarr = ResourceRecord "example.com." SOA 1 3600 soard+ in defaultResponse { question = [Question "hostmaster.example.com." A]+ , authority = [soarr] }++-- Expected compressed encoding of the 'test_soa' message+test_soa_bytes :: ByteString+test_soa_bytes = "0000858000010000000100000a686f73746d6173746572076578616d706c6503636f6d0000010001c0170006000100000e10001c036e7331c017c00c0000000000000000000000000000000000000000"++----------------------------------------------------------------++spec :: Spec+spec = do+ describe "decode" $ do+ it "decodes double pointers correctly" $+ tripleDecodeTest test_doublePointer+ it "decodes dname" $+ tripleDecodeTest test_dname+ it "decodes txt" $+ tripleDecodeTest test_txt+ it "decodes mx" $+ tripleDecodeTest test_mx+ it "detect excess" $+ case decode (encode defaultQuery <> "\0") of+ Left (DecodeError {}) -> True+ _ -> error "Excess input not detected"+ it "detect truncation" $+ case decode (BC.init $ encode defaultQuery) of+ Left (DecodeError {}) -> True+ _ -> error "Excess input not detected"+ it "soa mailbox presentation form" $+ case encode test_soa_in of+ enc | enc /= fromHexString test_soa_bytes+ -> error "Unexpected test_soa encoding"+ | otherwise -> case decode enc of+ Left err -> error $ "Error decoding test_soa: " ++ show err+ Right m | m /= test_soa_out+ -> error $ "Wrong decode of test_soa: " ++ show m+ | otherwise -> True++tripleDecodeTest :: ByteString -> IO ()+tripleDecodeTest hexbs =+ ecase (decode $ fromHexString hexbs) fail' $ \ x1 ->+ ecase (decode $ encode x1) fail' $ \ x2 ->+ ecase (decode $ encode x2) fail' $ \ x3 ->+ x3 `shouldBe` x2+ where+ fail' (DecodeError err) = fail err+ fail' _ = error "fail'"++ecase :: Either a b -> (a -> c) -> (b -> c) -> c+ecase (Left a) f _ = f a+ecase (Right b) _ g = g b++----------------------------------------------------------------++fromHexString :: ByteString -> ByteString+fromHexString (PS fptr off len) = unsafeCreate size $ \dst ->+ withForeignPtr fptr $ \src -> go (src `plusPtr` off) dst 0+ where+ size = len `div` 2+ go from to bytes+ | bytes == size = return ()+ | otherwise = do+ w1 <- peek from+ w2 <- peekByteOff from 1+ let w = hex2w (w1,w2)+ poke to w+ go (from `plusPtr` 2) (to `plusPtr` 1) (bytes + 1)++hex2w :: (Word8, Word8) -> Word8+hex2w (w1,w2) = h2w w1 * 16 + h2w w2++h2w :: Word8 -> Word8+h2w w+ | isDigit w = w - _0+ | otherwise = w - _a + 10
test/EncodeSpec.hs view
@@ -3,31 +3,33 @@ module EncodeSpec where import Data.IP-import Network.DNS-import Network.DNS.Internal (defaultQuery, makeQuestion) import Test.Hspec +import Network.DNS+ spec :: Spec spec = do describe "encode" $ do- it "encodes DNSFormat correctly" $ do+ it "encodes DNSMessage correctly" $ do check1 testQueryA check1 testQueryAAAA check1 testResponseA+ check1 testResponseTXT describe "decode" $ do- it "decodes DNSFormat correctly" $ do+ it "decodes DNSMessage correctly" $ do check2 testQueryA check2 testQueryAAAA check2 testResponseA+ check2 testResponseTXT -check1 :: DNSFormat -> Expectation+check1 :: DNSMessage -> Expectation check1 inp = out `shouldBe` Right inp where bs = encode inp out = decode bs -check2 :: DNSFormat -> Expectation+check2 :: DNSMessage -> Expectation check2 inp = bs' `shouldBe` bs where bs = encode inp@@ -37,26 +39,24 @@ defaultHeader :: DNSHeader defaultHeader = header defaultQuery -testQueryA :: DNSFormat+testQueryA :: DNSMessage testQueryA = defaultQuery { header = defaultHeader { identifier = 1000- , qdCount = 1 }- , question = [makeQuestion "www.mew.org." A]+ , question = [Question "www.mew.org." A] } -testQueryAAAA :: DNSFormat+testQueryAAAA :: DNSMessage testQueryAAAA = defaultQuery { header = defaultHeader {- identifier = 1000- , qdCount = 1+ identifier = 1001 }- , question = [makeQuestion "www.mew.org." AAAA]+ , question = [Question "www.mew.org." AAAA] } -testResponseA :: DNSFormat-testResponseA = DNSFormat {+testResponseA :: DNSMessage+testResponseA = DNSMessage { header = DNSHeader { identifier = 61046 , flags = DNSFlags {@@ -67,116 +67,71 @@ , recDesired = True , recAvailable = True , rcode = NoErr+ , authenData = False+ , chkDisable = False }- , qdCount = 1- , anCount = 8- , nsCount = 2- , arCount = 4 }+ , ednsHeader = NoEDNS , question = [Question { qname = "492056364.qzone.qq.com." , qtype = A } ]- , answer = [ResourceRecord {- rrname = "492056364.qzone.qq.com."- , rrtype = A- , rrttl = 568- , rdlen = 4- , rdata = RD_A $ toIPv4 [119, 147, 15, 122]- }- , ResourceRecord {- rrname = "492056364.qzone.qq.com."- , rrtype = A- , rrttl = 568- , rdlen = 4- , rdata = RD_A $ toIPv4 [119, 147, 79, 106]- }- , ResourceRecord {- rrname = "492056364.qzone.qq.com."- , rrtype = A- , rrttl = 568- , rdlen = 4- , rdata = RD_A $ toIPv4 [183, 60, 55, 43]- }- , ResourceRecord {- rrname = "492056364.qzone.qq.com."- , rrtype = A- , rrttl = 568- , rdlen = 4- , rdata = RD_A $ toIPv4 [183, 60, 55, 107]- }- , ResourceRecord {- rrname = "492056364.qzone.qq.com."- , rrtype = A- , rrttl = 568- , rdlen = 4- , rdata = RD_A $ toIPv4 [113, 108, 7, 172]- }- , ResourceRecord {- rrname = "492056364.qzone.qq.com."- , rrtype = A- , rrttl = 568- , rdlen = 4- , rdata = RD_A $ toIPv4 [113, 108, 7, 174]- }- , ResourceRecord {- rrname = "492056364.qzone.qq.com."- , rrtype = A- , rrttl = 568- , rdlen = 4- , rdata = RD_A $ toIPv4 [113, 108, 7, 175]- }- , ResourceRecord {- rrname = "492056364.qzone.qq.com."- , rrtype = A- , rrttl = 568- , rdlen = 4- , rdata = RD_A $ toIPv4 [119, 147, 15, 100]- }- ]- , authority = [ ResourceRecord {- rrname = "qzone.qq.com."- , rrtype = NS- , rrttl = 45919- , rdlen = 10- , rdata = RD_NS "ns-tel2.qq.com."- }- , ResourceRecord {- rrname = "qzone.qq.com."- , rrtype = NS- , rrttl = 45919- , rdlen = 10- , rdata = RD_NS "ns-tel1.qq.com."- }+ , answer =+ [ ResourceRecord "492056364.qzone.qq.com." A classIN 568 (RD_A $ toIPv4 [119, 147, 15, 122])+ , ResourceRecord "492056364.qzone.qq.com." A classIN 568 (RD_A $ toIPv4 [119, 147, 79, 106])+ , ResourceRecord "492056364.qzone.qq.com." A classIN 568 (RD_A $ toIPv4 [183, 60, 55, 43])+ , ResourceRecord "492056364.qzone.qq.com." A classIN 568 (RD_A $ toIPv4 [183, 60, 55, 107])+ , ResourceRecord "492056364.qzone.qq.com." A classIN 568 (RD_A $ toIPv4 [113, 108, 7, 172])+ , ResourceRecord "492056364.qzone.qq.com." A classIN 568 (RD_A $ toIPv4 [113, 108, 7, 174])+ , ResourceRecord "492056364.qzone.qq.com." A classIN 568 (RD_A $ toIPv4 [113, 108, 7, 175])+ , ResourceRecord "492056364.qzone.qq.com." A classIN 568 (RD_A $ toIPv4 [119, 147, 15, 100])+ ]+ , authority =+ [ ResourceRecord "qzone.qq.com." NS classIN 45919 (RD_NS "ns-tel2.qq.com.")+ , ResourceRecord "qzone.qq.com." NS classIN 45919 (RD_NS "ns-tel1.qq.com.")+ ]+ , additional =+ [ ResourceRecord "ns-tel1.qq.com." A classIN 46520 (RD_A $ toIPv4 [121, 14, 73, 115])+ , ResourceRecord "ns-tel2.qq.com." A classIN 2890 (RD_A $ toIPv4 [222, 73, 76, 226])+ , ResourceRecord "ns-tel2.qq.com." A classIN 2890 (RD_A $ toIPv4 [183, 60, 3, 202])+ , ResourceRecord "ns-tel2.qq.com." A classIN 2890 (RD_A $ toIPv4 [218, 30, 72, 180])+ ]+ }++testResponseTXT :: DNSMessage+testResponseTXT = DNSMessage {+ header = DNSHeader {+ identifier = 48724+ , flags = DNSFlags {+ qOrR = QR_Response+ , opcode = OP_STD+ , authAnswer = False+ , trunCation = False+ , recDesired = True+ , recAvailable = True+ , rcode = NoErr+ , authenData = False+ , chkDisable = False+ }+ }+ , ednsHeader = EDNSheader defaultEDNS+ , question = [Question {+ qname = "492056364.qzone.qq.com."+ , qtype = TXT+ } ]- , additional = [ ResourceRecord {- rrname = "ns-tel1.qq.com."- , rrtype = A- , rrttl = 46520- , rdlen = 4- , rdata = RD_A $ toIPv4 [121, 14, 73, 115]- }- , ResourceRecord {- rrname = "ns-tel2.qq.com."- , rrtype = A- , rrttl = 2890- , rdlen = 4- , rdata = RD_A $ toIPv4 [222, 73, 76, 226]- }- , ResourceRecord {- rrname = "ns-tel2.qq.com."- , rrtype = A- , rrttl = 2890- , rdlen = 4- , rdata = RD_A $ toIPv4 [183, 60, 3, 202]- }- , ResourceRecord {- rrname = "ns-tel2.qq.com."- , rrtype = A- , rrttl = 2890- , rdlen = 4- , rdata = RD_A $ toIPv4 [218, 30, 72, 180]- }- ]+ , answer =+ [ ResourceRecord "492056364.qzone.qq.com." TXT classIN 0 (RD_TXT "simple txt line")+ ]+ , authority =+ [ ResourceRecord "qzone.qq.com." NS classIN 45919 (RD_NS "ns-tel2.qq.com.")+ , ResourceRecord "qzone.qq.com." NS classIN 45919 (RD_NS "ns-tel1.qq.com.")+ ]+ , additional =+ [ ResourceRecord "ns-tel1.qq.com." A classIN 46520 (RD_A $ toIPv4 [121, 14, 73, 115])+ , ResourceRecord "ns-tel2.qq.com." A classIN 2890 (RD_A $ toIPv4 [222, 73, 76, 226])+ , ResourceRecord "ns-tel2.qq.com." A classIN 2890 (RD_A $ toIPv4 [183, 60, 3, 202])+ , ResourceRecord "ns-tel2.qq.com." A classIN 2890 (RD_A $ toIPv4 [218, 30, 72, 180])+ ] }
+ test/RoundTripSpec.hs view
@@ -0,0 +1,267 @@+{-# LANGUAGE OverloadedStrings, TransformListComp #-}++module RoundTripSpec where++import qualified Data.IP+import Data.IP (Addr, IP(..), IPv4, IPv6, toIPv4, toIPv6, makeAddrRange)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BS+import qualified Data.CaseInsensitive as CI+import Test.Hspec+import Test.Hspec.QuickCheck+import Test.QuickCheck (Gen, arbitrary, elements, forAll, frequency, listOf, oneof)+import GHC.Exts (the, groupWith)++import Network.DNS.Decode+import Network.DNS.Decode.Internal+import Network.DNS.Encode+import Network.DNS.Encode.Internal+import Network.DNS.Imports+import Network.DNS.Types++spec :: Spec+spec = do+ prop "IPv4" . forAll genIPv4 $ \ ip4 -> do+ let str = show ip4+ read str `shouldBe` ip4+ show (read str :: IPv4) `shouldBe` str++ prop "IPv6" . forAll genIPv6 $ \ ip6 -> do+ let str = show ip6+ read str `shouldBe` ip6+ show (read str :: IPv6) `shouldBe` str++ prop "TYPE" . forAll genTYPE $ \ t ->+ toTYPE (fromTYPE t) `shouldBe` t++ prop "Domain" . forAll genDomain $ \ dom -> do+ let bs = encodeDomain dom+ decodeDomain bs `shouldBe` Right dom+ fmap encodeDomain (decodeDomain bs) `shouldBe` Right bs++ prop "Mailbox" . forAll genMailbox $ \ dom -> do+ let bs = encodeMailbox dom+ decodeMailbox bs `shouldBe` Right dom+ fmap encodeMailbox (decodeMailbox bs) `shouldBe` Right bs++ prop "DNSFlags" . forAll (genDNSFlags 0x0f) $ \ flgs -> do+ let bs = encodeDNSFlags flgs+ decodeDNSFlags bs `shouldBe` Right flgs+ fmap encodeDNSFlags (decodeDNSFlags bs) `shouldBe` Right bs++ prop "ResourceRecord" . forAll genResourceRecord $ \ rr -> do+ let bs = encodeResourceRecord rr+ decodeResourceRecord bs `shouldBe` Right rr+ fmap encodeResourceRecord (decodeResourceRecord bs) `shouldBe` Right bs++ prop "DNSHeader" . forAll (genDNSHeader 0x0f) $ \ hdr ->+ decodeDNSHeader (encodeDNSHeader hdr) `shouldBe` Right hdr++ prop "DNSMessage" . forAll genDNSMessage $ \ msg ->+ decode (encode msg) `shouldBe` Right msg++ prop "EDNS" . forAll genEDNSHeader $ \(edns, hdr) -> do+ let eh = EDNSheader edns+ Right m = decode. encode $ DNSMessage hdr eh [] [] [] []+ ednsHeader m `shouldBe` eh++----------------------------------------------------------------++genDNSMessage :: Gen DNSMessage+genDNSMessage =+ DNSMessage <$> genDNSHeader 0x0f <*> makeEDNS <*> listOf genQuestion+ <*> listOf genResourceRecord <*> listOf genResourceRecord+ <*> listOf genResourceRecord+ where+ makeEDNS :: Gen EDNSheader+ makeEDNS = genBool >>= \t ->+ if t then EDNSheader <$> genEDNS+ else pure NoEDNS+++genQuestion :: Gen Question+genQuestion = Question <$> genDomain <*> genTYPE++genTYPE :: Gen TYPE+genTYPE = frequency+ [ (20, elements+ [ A, AAAA, NS, TXT, MX, CNAME, SOA, PTR, SRV, DNAME, OPT, DS, RRSIG+ , NSEC, DNSKEY, NSEC3, NSEC3PARAM, TLSA, CDS, CDNSKEY, CSYNC+ ])+ , (1, toTYPE <$> genWord16)+ ]++genResourceRecord :: Gen ResourceRecord+genResourceRecord = frequency+ [ (8, genRR)+ ]+ where+ genRR = do+ dom <- genDomain+ t <- elements [A, AAAA, NS, TXT, MX, CNAME, SOA, PTR, SRV, DNAME, DS,+ TLSA, NSEC, NSEC3]+ ResourceRecord dom t classIN <$> genWord32 <*> mkRData dom t++mkRData :: Domain -> TYPE -> Gen RData+mkRData dom typ =+ case typ of+ A -> RD_A <$> genIPv4+ AAAA -> RD_AAAA <$> genIPv6+ NS -> pure $ RD_NS dom+ TXT -> RD_TXT <$> genTextString+ MX -> RD_MX <$> genWord16 <*> genDomain+ CNAME -> pure $ RD_CNAME dom+ SOA -> RD_SOA dom <$> genMailbox <*> genWord32 <*> genWord32 <*> genWord32 <*> genWord32 <*> genWord32+ PTR -> RD_PTR <$> genDomain+ SRV -> RD_SRV <$> genWord16 <*> genWord16 <*> genWord16 <*> genDomain+ DNAME -> RD_DNAME <$> genDomain+ DS -> RD_DS <$> genWord16 <*> genWord8 <*> genWord8 <*> genByteString+ NSEC -> RD_NSEC <$> genDomain <*> genNsecTypes+ NSEC3 -> genNSEC3+ TLSA -> RD_TLSA <$> genWord8 <*> genWord8 <*> genWord8 <*> genByteString+ CAA -> RD_CAA <$> genWord8 <*> (CI.mk <$> genByteString) <*> genByteString++ _ -> pure . RD_TXT $ "Unhandled type " <> BS.pack (show typ)+ where+ genNSEC3 = do+ (alg, hlen) <- elements [(1,32),(2,64)]+ flgs <- elements [0,1]+ iter <- elements [0..100]+ salt <- elements ["", "AB"]+ hash <- B.pack <$> replicateM hlen genWord8+ RD_NSEC3 alg flgs iter salt hash <$> genNsecTypes+ genTextString = do+ len <- elements [0, 1, 63, 255, 256, 511, 512, 1023, 1024]+ B.pack <$> replicateM len genWord8+ genNsecTypes = do+ ntypes <- elements [0..15]+ types <- sequence $ replicate ntypes $ toTYPE <$> elements [1..1024]+ return $ [ the t |+ t <- types,+ then group by (fromTYPE t)+ using groupWith ]++genIPv4 :: Gen IPv4+genIPv4 = toIPv4 <$> replicateM 4 (fromIntegral <$> genWord8)++genIPv6 :: Gen IPv6+genIPv6 = toIPv6 <$> replicateM 8 (fromIntegral <$> genWord16)++genByteString :: Gen BS.ByteString+genByteString = elements+ [ "", "a", "a.b", "abc", "a.b.c", "a\\.b.c", "\\001.a.b", "\\$.a.b" ]++genMboxString :: Gen BS.ByteString+genMboxString = elements+ [ "", "a", "a@b", "abc", "a@b.c", "first.last@example.org" ]++genDomain :: Gen Domain+genDomain = do+ bs <- genByteString+ pure $ bs <> "."++genMailbox :: Gen Mailbox+genMailbox = do+ bs <- genMboxString+ pure $ bs <> "."++genDNSHeader :: Word16 -> Gen DNSHeader+genDNSHeader maxrc = DNSHeader <$> genWord16 <*> genDNSFlags maxrc++genDNSFlags :: Word16 -> Gen DNSFlags+genDNSFlags maxrc =+ DNSFlags <$> genQorR <*> genOPCODE <*> genBool <*> genBool+ <*> genBool <*> genBool <*> genRCODE maxrc <*> genBool <*> genBool++genWord16 :: Gen Word16+genWord16 = arbitrary++genWord32 :: Gen Word32+genWord32 = arbitrary++genWord8 :: Gen Word8+genWord8 = arbitrary++genBool :: Gen Bool+genBool = elements [True, False]++genQorR :: Gen QorR+genQorR = elements [minBound .. maxBound]++genOPCODE :: Gen OPCODE+genOPCODE = elements [OP_STD, OP_INV, OP_SSR, OP_NOTIFY, OP_UPDATE]++genRCODE :: Word16 -> Gen RCODE+genRCODE maxrc = elements $ map toRCODE [0..maxrc]++genEDNS :: Gen EDNS+genEDNS = do+ vers <- genWord8+ ok <- genBool+ od <- genOData+ us <- elements [minUdpSize..maxUdpSize]+ return $ defaultEDNS {+ ednsVersion = vers+ , ednsUdpSize = us+ , ednsDnssecOk = ok+ , ednsOptions = [od]+ }++genOData :: Gen OData+genOData = oneof+ [ genOD_Unknown+ , genOD_ECS+ ]+ where+ -- | Choose from the range reserved for local use+ -- https://tools.ietf.org/html/rfc6891#section-9+ genOD_Unknown = UnknownOData <$> elements [65001, 65534] <*> genByteString++ -- | Only valid ECS prefixes round-trip, make sure the prefix is+ -- is consistent with the mask.+ genOD_ECS = do+ usev4 <- genBool+ if usev4+ then genFuzzed genIPv4 IPv4 Data.IP.fromIPv4 1 32+ else genFuzzed genIPv6 IPv6 Data.IP.fromIPv6b 2 128+ where+ genFuzzed :: Addr a+ => Gen a+ -> (a -> IP)+ -> (a -> [Int])+ -> Word16+ -> Word8+ -> Gen OData+ genFuzzed gen toIP toBytes fam alen = do+ ip <- gen+ bits1 <- elements [1 .. alen]+ bits2 <- elements [0 .. alen]+ fuzzSrcBits <- genBool+ fuzzScpBits <- genBool+ srcBits <- if not fuzzSrcBits+ then pure bits1+ else flip mod alen. (+) bits1 <$> elements [1..alen-1]+ scpBits <- if not fuzzScpBits+ then pure bits2+ else elements [alen+1 .. 0xFF]+ let addr = Data.IP.addr. makeAddrRange ip $ fromIntegral bits1+ bytes = map fromIntegral $ toBytes addr+ len = (fromIntegral bits1 + 7) `div` 8+ less = take (len - 1) bytes+ more = less ++ [0xFF]+ if srcBits == bits1+ then if scpBits == bits2+ then pure $ OD_ClientSubnet bits1 scpBits $ toIP addr+ else pure $ OD_ECSgeneric fam bits1 scpBits $ B.pack bytes+ else if srcBits < bits1+ then pure $ OD_ECSgeneric fam srcBits scpBits $ B.pack more+ else pure $ OD_ECSgeneric fam srcBits scpBits $ B.pack less++genExtRCODE :: Gen RCODE+genExtRCODE = elements $ map toRCODE [0..4095]++genEDNSHeader :: Gen (EDNS, DNSHeader)+genEDNSHeader = do+ edns <- genEDNS+ hdr <- genDNSHeader 0xF00+ return (edns, hdr)
+ test2/IOSpec.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE OverloadedStrings #-}++module IOSpec where++import Network.Socket hiding (send)+import Test.Hspec++import Network.DNS.IO as DNS+import Network.DNS.Imports+import Network.DNS.Types as DNS++spec :: Spec+spec = describe "send/receive" $ do++ it "resolves well with UDP" $ do+ sock <- connectedSocket Datagram+ -- Google's resolvers support the AD and CD bits+ let qry = encodeQuestion 1 (Question "www.mew.org" A) $+ adFlag FlagSet <> ednsEnabled FlagClear+ send sock qry+ ans <- receive sock+ identifier (header ans) `shouldBe` 1++ it "resolves well with TCP" $ do+ sock <- connectedSocket Stream+ let qry = encodeQuestion 1 (Question "www.mew.org" A) $+ adFlag FlagClear <> cdFlag FlagSet <> doFlag FlagSet+ sendVC sock qry+ ans <- receiveVC sock+ identifier (header ans) `shouldBe` 1++connectedSocket :: SocketType -> IO Socket+connectedSocket typ = do+ let hints = defaultHints { addrFamily = AF_INET, addrSocketType = typ, addrFlags = [AI_NUMERICHOST]}+ addr:_ <- getAddrInfo (Just hints) (Just "8.8.8.8") (Just "domain")+ sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)+ connect sock $ addrAddress addr+ return sock
test2/LookupSpec.hs view
@@ -2,66 +2,58 @@ module LookupSpec where -import Control.Applicative-import qualified Data.ByteString.Char8 as BS-import Data.List-import Network.DNS as DNS import Test.Hspec +import Network.DNS as DNS+ spec :: Spec-spec = do- describe "lookupA" $ do- it "gets IPv4 addresses" $ do- rs <- makeResolvSeed defaultResolvConf- withResolver rs $ \resolver ->- DNS.lookupA resolver "www.mew.org" `shouldReturn` Just ["202.232.15.101"]+spec = describe "lookup" $ do - it "gets IPv4 addresses via CNAME" $ do- rs <- makeResolvSeed defaultResolvConf- withResolver rs $ \resolver ->- DNS.lookupA resolver "www.kame.net" `shouldReturn` Just ["203.178.141.194"]+ it "lookupA" $ do+ rs <- makeResolvSeed defaultResolvConf+ withResolver rs $ \resolver -> do+ addrs <- DNS.lookupA resolver "mew.org"+ -- mew.org has one or more IPv6 addresses+ fmap null addrs `shouldBe` Right False - describe "lookupAAAA" $ do- it "gets IPv6 addresses" $ do- rs <- makeResolvSeed defaultResolvConf- withResolver rs $ \resolver -> do- DNS.lookupAAAA resolver "mew.org" `shouldReturn` Nothing- DNS.lookupAAAA resolver "www.mew.org" `shouldReturn` Just ["2001:240:11e:c00::101"]+ it "lookupAAAA" $ do+ rs <- makeResolvSeed defaultResolvConf+ withResolver rs $ \resolver -> do+ -- google.com has one or more IPv6 addresses+ addrs <- DNS.lookupAAAA resolver "google.com"+ fmap null addrs `shouldBe` Right False - describe "lookupNS" $ do- it "gets NS" $ do- rs <- makeResolvSeed defaultResolvConf- withResolver rs $ \resolver -> do- actual <- DNS.lookupNS resolver "mew.org"- let expected = Just ["ns1.mew.org.", "ns2.mew.org."]- -- The order of NS records is variable, so we sort the- -- result.- sort <$> actual `shouldBe` expected+ it "lookupAAAA with emty result" $ do+ rs <- makeResolvSeed defaultResolvConf+ withResolver rs $ \resolver -> do+ addrs <- DNS.lookupAAAA resolver "ipv4.tlund.se"+ -- mew.org does not have any IPv6 addresses+ fmap null addrs `shouldBe` Right True - describe "lookupTXT" $ do- it "gets TXT" $ do- rs <- makeResolvSeed defaultResolvConf- withResolver rs $ \resolver ->- DNS.lookupTXT resolver "mew.org" `shouldReturn` Just ["v=spf1 +mx -all"]+ it "lookupMX" $ do+ rs <- makeResolvSeed defaultResolvConf+ withResolver rs $ \resolver -> do+ addrs <- DNS.lookupMX resolver "mew.org"+ -- mew.org has one or more MX records.+ fmap null addrs `shouldBe` Right False - describe "lookupAviaMX" $ do- it "gets IPv4 addresses via MX" $ do- rs <- makeResolvSeed defaultResolvConf- withResolver rs $ \resolver -> do- as <- DNS.lookupAviaMX resolver "mixi.jp"- sort <$> as `shouldBe` Just ["202.32.29.4", "202.32.29.5"]+ it "lookupTXT" $ do+ rs <- makeResolvSeed defaultResolvConf+ withResolver rs $ \resolver -> do+ addrs <- DNS.lookupTXT resolver "mew.org"+ -- mew.org has one or more TXT records.+ fmap null addrs `shouldBe` Right False - describe "lookupPTR" $ do- it "gets PTR" $ do- rs <- makeResolvSeed defaultResolvConf- withResolver rs $ \resolver -> do- let target = "210.130.137.80"- rev = BS.intercalate "." (reverse (BS.split '.' target))- `BS.append` ".in-addr.arpa"- DNS.lookupPTR resolver rev `shouldReturn` Just ["www-v4.iij.ad.jp."]+ it "lookupSOA" $ do+ rs <- makeResolvSeed defaultResolvConf+ withResolver rs $ \resolver -> do+ addrs <- DNS.lookupTXT resolver "mew.org"+ -- mew.org has a SOA record.+ fmap null addrs `shouldBe` Right False - describe "lookupSRV" $ do- it "gets SRV" $ do- rs <- makeResolvSeed defaultResolvConf- withResolver rs $ \resolver ->- DNS.lookupSRV resolver "_sip._tcp.cisco.com" `shouldReturn` Just [(1,0,5060,"vcsgw.cisco.com.")]+ it "lookupNS" $ do+ rs <- makeResolvSeed defaultResolvConf+ withResolver rs $ \resolver -> do+ addrs <- DNS.lookupNS resolver "mew.org"+ -- mew.org has one or more NS records.+ fmap null addrs `shouldBe` Right False