dns-patterns-0.2.0: lib/Network/DNS/Pattern.hs
-- |
-- Module : Network.DNS.Pattern
-- Description : DNS pattern matching
--
-- Patterns can be simple absolute domain names, where labels are replaceable with a single glob @*@ or a globstar @**@.
-- A single glob will match any label in its place, where globstar will greedily match as many labels as possible towards the left.
--
-- Admits the escape sequences from domain names. See 'Network.DNS'.
--
-- Note: Currently a globstar is only supported on the left-most label.
--
-- Examples of valid patterns are:
--
-- @
-- *.foo.bar.
-- **.foo.bar.
-- foo.*.bar.
-- foo.bar.*.
-- @
{-# LANGUAGE OverloadedStrings #-}
module Network.DNS.Pattern
( -- * Pattern language
DomainPattern
, LabelPattern
, matchesPattern
, patternWorksInside
, labelMatchesPattern
-- * Parsing
, parsePattern
, patternP
-- * Pretty printing
, pprPattern
, pprPatternCF
, pprLabelPattern
, pprLabelPatternCF
)
where
import Data.Foldable (asum)
import Control.Applicative.Combinators
import qualified Data.Attoparsec.Text as A
import qualified Data.Text as T
import Network.DNS
import Network.DNS.Internal
import Network.DNS.Pattern.Internal
-- | Print domain pattern.
--
-- This function nearly roundtrips with 'parsePattern' up to escape sequence equivalence.
--
-- prop> parsePattern . pprPattern ~~~ id
pprPattern :: DomainPattern -> T.Text
pprPattern (DomainPattern l) = T.pack (fromDList build)
where
build = foldr (\x buf -> buildLabelPattern x <> singleton '.' <> buf) mempty l
-- | Print domain pattern after into presentation format after case-folding according to [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3).
--
-- This function nearly roundtrips with 'parsePattern' up to escape sequence equivalence and case folding.
--
-- prop> parsePattern . pprPatternCF ~~~ id
pprPatternCF :: DomainPattern -> T.Text
pprPatternCF (DomainPattern l) = T.pack (fromDList build)
where
build = foldr (\x buf -> buildLabelPatternCF x <> singleton '.' <> buf) mempty l
-- | Print a singular domain label pattern into a presentation format.
pprLabelPattern :: LabelPattern -> T.Text
pprLabelPattern = T.pack . fromDList . buildLabelPattern
-- | Print a singular domain label pattern into a presentation format after case-folding according to [RFC4343](https://datatracker.ietf.org/doc/html/rfc4343#section-3).
pprLabelPatternCF :: LabelPattern -> T.Text
pprLabelPatternCF = T.pack . fromDList . buildLabelPatternCF
{-# INLINE buildLabelPattern #-}
buildLabelPattern :: LabelPattern -> DList Char
buildLabelPattern (DomLiteral d) = buildLabel d
buildLabelPattern DomGlob = singleton '*'
buildLabelPattern DomGlobStar = toDList "**"
{-# INLINE buildLabelPatternCF #-}
buildLabelPatternCF :: LabelPattern -> DList Char
buildLabelPatternCF (DomLiteral d) = buildLabelCF d
buildLabelPatternCF DomGlob = singleton '*'
buildLabelPatternCF DomGlobStar = toDList "**"
-- | Given a pattern and a DNS zone specified by a domain name, test whether or not the pattern
-- is applicable beneath that zone.
--
-- @
-- foo.*.bar. applicable inside zone quux.bar.
-- foo.bar. applicable inside zone bar.
-- bar. applicable inside zone bar.
-- foo.bar. not applicable inside zone quux.
-- @
patternWorksInside :: DomainPattern -> Domain -> Bool
patternWorksInside (DomainPattern x) (Domain y) = go (reverse x) (reverse y)
where
go :: [LabelPattern] -> [DomainLabel] -> Bool
go [DomGlobStar] _ = True
go [] [] = True
go [] _ls = False
go _p [] = True
go (p:ps) (l:ls) = labelMatchesPattern l p && go ps ls
-- | Test whether a given domain matches a 'DomainPattern'
matchesPattern :: Domain -> DomainPattern -> Bool
matchesPattern (Domain x) (DomainPattern y) = go (reverse x) (reverse y)
where
go :: [DomainLabel] -> [LabelPattern] -> Bool
go [] [] = True
go [] _ps = False
go _ls [] = False
go _ls [DomGlobStar] = True
go (l:ls) (p:ps) = labelMatchesPattern l p && go ls ps
-- | Test whether a single label matches a label pattern
labelMatchesPattern :: DomainLabel -> LabelPattern -> Bool
labelMatchesPattern _l DomGlob = True
labelMatchesPattern l (DomLiteral p) = l == p
labelMatchesPattern _l DomGlobStar = True
-- | Attoparsec 'A.Parser' for domain patterns. See 'parsePattern' for a convenince wrapper.
patternP :: A.Parser DomainPattern
patternP = asum [ do p <- litGlobStar <* A.char '.'
ps <- litGlob `endBy` A.char '.'
pure (DomainPattern (p:ps))
, DomainPattern [] <$ A.char '.' -- Literal pattern of the root domain "."
]
where
litGlobStar :: A.Parser LabelPattern
litGlobStar = asum [ DomLiteral <$> domainLabelP
, DomGlobStar <$ A.string "**"
, DomGlob <$ A.char '*'
]
litGlob :: A.Parser LabelPattern
litGlob = asum [ DomLiteral <$> domainLabelP
, DomGlob <$ A.char '*'
]
-- | Parse a domain pattern. Convenience wrapper for 'patternP.
parsePattern :: T.Text -> Either String DomainPattern
parsePattern = A.parseOnly (patternP <* A.endOfInput)