microdns-0.1.0.0: src/MicroDNS/MicroZone.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
module MicroDNS.MicroZone (
ZoneFile (..),
Directive (..),
-- * parsing
zoneFile,
-- * exploitation
collectDirectives,
) where
import qualified Data.ByteString as ByteString
import Data.CaseInsensitive as CI
import Data.IP (IPv4, IPv6)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Void (Void)
import Data.Word (Word16)
import qualified Network.DNS as DNS
import Text.Megaparsec
import Text.Megaparsec.Char (alphaNumChar, digitChar, hexDigitChar, newline, space, string)
import MicroDNS.Handler (Apex (..), apexFromText, apexify, endsWithDot)
data ZoneFile
= ZoneFile
{ directives :: [Directive]
}
deriving (Show)
type CommentedText = Text
data GuardedCommand
= SetApex Text
deriving (Show)
data Directive
= Comment CommentedText
| Command GuardedCommand
| Record DNS.ResourceRecord
deriving (Show)
type Parser = Parsec Void Text
zoneFile :: Parser ZoneFile
zoneFile =
ZoneFile <$> (directive `sepEndBy` skipSome newline) <* eof
directive :: Parser Directive
directive =
choice
[ (Comment <$> comment)
, (Command <$> command)
, (Record <$> record)
]
comment :: Parser CommentedText
comment = string "--" *> space *> takeWhile1P Nothing ((/=) '\n')
command :: Parser GuardedCommand
command =
choice
[ SetApex <$> apex
]
where
apex = string "microdns:apex" *> space *> domainName
record :: Parser DNS.ResourceRecord
record =
choice
[ txtRecord
, -- aaaa first to avoid a valid 'a' prefix parse
aaaaRecord
, aRecord
, caaRecord
, cnameRecord
, mxRecord
, srvRecord
]
txtRecord :: Parser DNS.ResourceRecord
txtRecord = do
_ <- string "TXT"
_ <- space
!domain <- Text.encodeUtf8 <$> apexOrDomainName
_ <- space
!val <- Text.encodeUtf8 <$> quotedString
pure $ DNS.ResourceRecord domain DNS.TXT DNS.classIN 300 $ DNS.RD_TXT val
aRecord :: Parser DNS.ResourceRecord
aRecord = do
_ <- string "A"
_ <- space
!domain <- Text.encodeUtf8 <$> apexOrDomainName
_ <- space
!val <- ipv4
pure $ DNS.ResourceRecord domain DNS.A DNS.classIN 300 $ DNS.RD_A val
aaaaRecord :: Parser DNS.ResourceRecord
aaaaRecord = do
_ <- string "AAAA"
_ <- space
!domain <- Text.encodeUtf8 <$> apexOrDomainName
_ <- space
!val <- ipv6
pure $ DNS.ResourceRecord domain DNS.AAAA DNS.classIN 300 $ DNS.RD_AAAA val
caaRecord :: Parser DNS.ResourceRecord
caaRecord = do
_ <- string "CAA"
_ <- space
!domain <- Text.encodeUtf8 <$> apexOrDomainName
_ <- space
!key <- CI.mk . Text.encodeUtf8 <$> quotedString
_ <- space
!val <- Text.encodeUtf8 <$> quotedString
pure $ DNS.ResourceRecord domain DNS.CAA DNS.classIN 300 $ DNS.RD_CAA 0 key val
cnameRecord :: Parser DNS.ResourceRecord
cnameRecord = do
_ <- string "CNAME"
_ <- space
!domain <- Text.encodeUtf8 <$> apexOrDomainName
_ <- space
!val <- Text.encodeUtf8 <$> domainName
pure $ DNS.ResourceRecord domain DNS.CNAME DNS.classIN 300 $ DNS.RD_CNAME val
mxRecord :: Parser DNS.ResourceRecord
mxRecord = do
_ <- string "MX"
_ <- space
!domain <- Text.encodeUtf8 <$> apexOrDomainName
_ <- space
!priority <- mxPriority
_ <- space
!val <- Text.encodeUtf8 <$> domainName
pure $ DNS.ResourceRecord domain DNS.MX DNS.classIN 300 $ DNS.RD_MX priority val
srvRecord :: Parser DNS.ResourceRecord
srvRecord = do
_ <- string "SRV"
_ <- space
!domain <- Text.encodeUtf8 <$> apexOrDomainName
_ <- space
!priority <- srvPriority
_ <- space
!port <- srvPortnum
_ <- space
!val <- Text.encodeUtf8 <$> domainName
pure $ DNS.ResourceRecord domain DNS.SRV DNS.classIN 300 $ DNS.RD_SRV priority 1 port val
apexOrDomainName :: Parser Text
apexOrDomainName =
(string "@" *> pure "") <|> domainName
domainName :: Parser Text
domainName =
Text.pack
<$> many (alphaNumChar <|> oneOf ['.', '-', '_'])
quotedString :: Parser Text
quotedString =
Text.pack
<$> between (string "\"") (string "\"") contents
where
contents :: Parser [Char]
contents = many (escapedChar <|> plainChar)
escapedChar :: Parser Char
escapedChar = backspace <|> quote
backspace :: Parser Char
backspace = string "\\\\" *> pure '\\'
quote :: Parser Char
quote = string "\\\"" *> pure '"'
plainChar :: Parser Char
plainChar = noneOf ['\\', '"']
ipv4 :: Parser IPv4
ipv4 = read <$> many (digitChar <|> oneOf ['.'])
ipv6 :: Parser IPv6
ipv6 = read <$> many (hexDigitChar <|> oneOf [':', '.'])
mxPriority, srvPriority, srvPortnum :: Parser Word16
mxPriority = read <$> many (digitChar)
srvPriority = read <$> many (digitChar)
srvPortnum = read <$> many (digitChar)
collectDirectives :: Apex -> ZoneFile -> [DNS.ResourceRecord]
collectDirectives defaultApex zones =
go defaultApex [] (directives zones)
where
go :: Apex -> [DNS.ResourceRecord] -> [Directive] -> [DNS.ResourceRecord]
go _ xs [] = xs
go a xs ((Comment _) : ds) = go a xs ds
go _ xs ((Command (SetApex a)) : ds) = go (apexFromText a) xs ds
go a xs ((Record rec) : ds) = go a (adapt a rec : xs) ds
adapt :: Apex -> DNS.ResourceRecord -> DNS.ResourceRecord
adapt apex rr
| matchSuffix apex rr = rr
| otherwise = rr{DNS.rrname = addApex apex (DNS.rrname rr)}
addApex :: Apex -> ByteString.ByteString -> ByteString.ByteString
addApex apex "" = getApex apex
addApex apex bs = if endsWithDot bs then mconcat [bs, getApex apex] else mconcat [bs, ".", getApex apex]
matchSuffix :: Apex -> DNS.ResourceRecord -> Bool
matchSuffix apex rr = getApex apex `ByteString.isSuffixOf` getApex (apexify $ DNS.rrname rr)