{-# LANGUAGE CPP #-}
module Data.X509.CertificateStore
( CertificateStore
, makeCertificateStore
, readCertificateStore
, findCertificate
, listCertificates
) where
import Data.Char (isDigit, isHexDigit)
import Data.Either (rights)
import Data.List (foldl', isPrefixOf)
#if MIN_VERSION_base(4,9,0)
import Data.Semigroup
#else
import Data.Monoid
#endif
import Data.PEM (pemParseBS, pemContent)
import Data.X509
import qualified Data.Map as M
import Control.Applicative ((<$>))
import Control.Monad (mplus, filterM)
import System.Directory (getDirectoryContents, doesFileExist, doesDirectoryExist)
import System.FilePath ((</>))
import qualified Control.Exception as E
import qualified Data.ByteString as B
data CertificateStore = CertificateStore (M.Map DistinguishedName SignedCertificate)
| CertificateStores [CertificateStore]
#if MIN_VERSION_base(4,9,0)
instance Semigroup CertificateStore where
(<>) = append
#endif
instance Monoid CertificateStore where
mempty = CertificateStore M.empty
#if !(MIN_VERSION_base(4,11,0))
mappend = append
#endif
append :: CertificateStore -> CertificateStore -> CertificateStore
append s1@(CertificateStore _) s2@(CertificateStore _) = CertificateStores [s1,s2]
append (CertificateStores l) s2@(CertificateStore _) = CertificateStores (l ++ [s2])
append s1@(CertificateStore _) (CertificateStores l) = CertificateStores ([s1] ++ l)
append (CertificateStores l1) (CertificateStores l2) = CertificateStores (l1 ++ l2)
makeCertificateStore :: [SignedCertificate] -> CertificateStore
makeCertificateStore = CertificateStore . foldl' accumulate M.empty
where accumulate m x509 = M.insert (certSubjectDN $ getCertificate x509) x509 m
findCertificate :: DistinguishedName -> CertificateStore -> Maybe SignedCertificate
findCertificate dn store = lookupIn store
where lookupIn (CertificateStore m) = M.lookup dn m
lookupIn (CertificateStores l) = foldl mplus Nothing $ map lookupIn l
listCertificates :: CertificateStore -> [SignedCertificate]
listCertificates (CertificateStore store) = map snd $ M.toList store
listCertificates (CertificateStores l) = concatMap listCertificates l
readCertificateStore :: FilePath -> IO (Maybe CertificateStore)
readCertificateStore path = do
isDir <- doesDirectoryExist path
isFile <- doesFileExist path
wrapStore <$> (if isDir then makeDirStore else if isFile then makeFileStore else return [])
where
wrapStore :: [SignedCertificate] -> Maybe CertificateStore
wrapStore [] = Nothing
wrapStore l = Just $ makeCertificateStore l
makeFileStore = readCertificates path
makeDirStore = do
certFiles <- listDirectoryCerts path
concat <$> mapM readCertificates certFiles
readCertificates :: FilePath -> IO [SignedCertificate]
readCertificates file = E.catch (either (const []) (rights . map getCert) . pemParseBS <$> B.readFile file) skipIOError
where
getCert = decodeSignedCertificate . pemContent
skipIOError :: E.IOException -> IO [SignedCertificate]
skipIOError _ = return []
listDirectoryCerts :: FilePath -> IO [FilePath]
listDirectoryCerts path =
getDirContents >>= filterM doesFileExist
where
isHashedFile s = length s == 10
&& isDigit (s !! 9)
&& (s !! 8) == '.'
&& all isHexDigit (take 8 s)
isCert x = (not $ isPrefixOf "." x) && (not $ isHashedFile x)
getDirContents = E.catch (map (path </>) . filter isCert <$> getDirectoryContents path) emptyPaths
where emptyPaths :: E.IOException -> IO [FilePath]
emptyPaths _ = return []