packages feed

domain-auth-0.2.5: Network/DomainAuth/SPF/Resolver.hs

{-# LANGUAGE OverloadedStrings #-}

module Network.DomainAuth.SPF.Resolver (
    resolveSPF,
    SpfLookup (..),
    dnsLookup,
    SpfCtx,
    newSpfCtx,
) where

import Control.Monad
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BS
import Data.IORef
import Data.IP
import Data.Maybe
import Network.DNS
import Network.DomainAuth.SPF.Parser
import Network.DomainAuth.SPF.Types

----------------------------------------------------------------

-- | DNS lookup functions used by SPF.
data SpfLookup = SpfLookup
    { spfLookupTXT :: Domain -> IO (Either DNSError [ByteString])
    , spfLookupA :: Domain -> IO (Either DNSError [IPv4])
    , spfLookupAAAA :: Domain -> IO (Either DNSError [IPv6])
    , spfLookupMX :: Domain -> IO (Either DNSError [(Domain, Int)])
    }

dnsLookup :: Resolver -> SpfLookup
dnsLookup r =
    SpfLookup
        { spfLookupTXT = lookupTXT r
        , spfLookupA = lookupA r
        , spfLookupAAAA = lookupAAAA r
        , spfLookupMX = lookupMX r
        }

-- | Shared state for one SPF evaluation.
data SpfCtx = SpfCtx
    { ctxLookup :: SpfLookup
    , ctxLimit :: Int
    , ctxCount :: IORef Int
    , ctxVoid :: IORef Int
    }

newSpfCtx :: SpfLookup -> Int -> IO SpfCtx
newSpfCtx lk lim = SpfCtx lk lim <$> newIORef 0 <*> newIORef 0

-- RFC 7208 Section 4.6.4
voidLookupLimit, mxLimit :: Int
voidLookupLimit = 2
mxLimit = 10

-- | Charging one DNS-querying term against the limit.
charge :: SpfCtx -> IO ()
charge ctx = do
    n <- atomicModifyIORef' (ctxCount ctx) $ \x -> (x + 1, x + 1)
    when (n > ctxLimit ctx) $ fail "PermError"

-- | NXDOMAIN is treated as empty. Empty answers are void lookups.
checkVoid :: SpfCtx -> Either DNSError [a] -> IO [a]
checkVoid ctx r = case r of
    Left NameError -> void' >> return []
    Left _ -> fail "TempError"
    Right [] -> void' >> return []
    Right xs -> return xs
  where
    void' = do
        n <- atomicModifyIORef' (ctxVoid ctx) $ \x -> (x + 1, x + 1)
        when (n > voidLookupLimit) $ fail "PermError"

----------------------------------------------------------------

-- | 'True' for include/redirect, where no SPF record is PermError.
resolveSPF :: SpfCtx -> Bool -> Domain -> IP -> IO [IO SpfSeq]
resolveSPF ctx nested dom ip = do
    jrc <- spfLookupTXT (ctxLookup ctx) dom
    rc <- case jrc of
        Left NameError -> return []
        Left _ -> fail "TempError"
        Right rc -> return rc
    let rr = getSPFRR rc
    checkExistence rr (if nested then "PermError" else "None")
    let jrs = parseSPF rr
    checkSyntax jrs "PermError"
    let is = filterSPFWithIP ip (fromJust jrs)
    return $ map (toSpfSeq ctx dom ip) is
  where
    getSPFRR rc = case filter ("v=spf1" `BS.isPrefixOf`) rc of
        [] -> ""
        t : _ -> t
    checkSyntax rs estr = when (isNothing rs) (fail estr)
    checkExistence rr estr = when (BS.null rr) (fail estr)

----------------------------------------------------------------

filterSPFWithIP :: IP -> [SPF] -> [SPF]
filterSPFWithIP (IPv4 _) spfs = filter exceptIPv4 spfs
filterSPFWithIP (IPv6 _) spfs = filter exceptIPv6 spfs

exceptIPv4 :: SPF -> Bool
exceptIPv4 (SPF_IPv6Range _ _) = False
exceptIPv4 _ = True

exceptIPv6 :: SPF -> Bool
exceptIPv6 (SPF_IPv4Range _ _) = False
exceptIPv6 _ = True

----------------------------------------------------------------

toSpfSeq :: SpfCtx -> Domain -> IP -> SPF -> IO SpfSeq
toSpfSeq _ _ _ (SPF_IPv4Range q ipr) = return $ SS_IPv4Range q ipr
toSpfSeq _ _ _ (SPF_IPv6Range q ipr) = return $ SS_IPv6Range q ipr
toSpfSeq _ _ _ (SPF_All q) = return $ SS_All q
toSpfSeq ctx _ ip (SPF_Include q dom) = do
    charge ctx
    SS_IF_Pass q <$> resolveSPF ctx True dom ip
toSpfSeq ctx _ ip (SPF_Redirect dom) = do
    charge ctx
    SS_SpfSeq <$> resolveSPF ctx True dom ip
toSpfSeq ctx dom0 ip (SPF_MX q mdom (l4, l6)) = do
    charge ctx
    mxs <- spfLookupMX lk dom >>= checkVoid ctx
    when (length mxs > mxLimit) $ fail "PermError"
    let hosts = map fst mxs
    case ip of
        IPv4 _ ->
            SS_IPv4Ranges q . ranges l4 . concat <$> mapM (lookupHost (spfLookupA lk)) hosts
        IPv6 _ ->
            SS_IPv6Ranges q . ranges l6 . concat
                <$> mapM (lookupHost (spfLookupAAAA lk)) hosts
  where
    lk = ctxLookup ctx
    dom = fromMaybe dom0 mdom
toSpfSeq ctx dom0 ip (SPF_Address q mdom (l4, l6)) = do
    charge ctx
    case ip of
        IPv4 _ -> SS_IPv4Ranges q . ranges l4 <$> (spfLookupA lk dom >>= checkVoid ctx)
        IPv6 _ -> SS_IPv6Ranges q . ranges l6 <$> (spfLookupAAAA lk dom >>= checkVoid ctx)
  where
    lk = ctxLookup ctx
    dom = fromMaybe dom0 mdom

ranges :: Addr a => Int -> [a] -> [AddrRange a]
ranges len = map (`makeAddrRange` len)

-- | Lookups for MX hosts are not counted as void lookups.
lookupHost :: (Domain -> IO (Either DNSError [a])) -> Domain -> IO [a]
lookupHost f d = do
    r <- f d
    case r of
        Left NameError -> return []
        Left _ -> fail "TempError"
        Right xs -> return xs