    module Propellor.Property.DnsSec where

    import Propellor.Base
    import qualified Propellor.Property.File as File

    -- | Puts the DNSSEC key files in place from PrivData.
    --
    -- signedPrimary uses this, so this property does not normally need to be
    -- used directly.
    keysInstalled :: Domain -> RevertableProperty (HasInfo + UnixLike) UnixLike
    keysInstalled domain = setup <!> cleanup
      where
            setup = propertyList "DNSSEC keys installed" $ toProps $
                    map installkey keys

            cleanup = propertyList "DNSSEC keys removed" $ toProps $
                    map (File.notPresent . keyFn domain) keys

            installkey k = writer (keysrc k) (keyFn domain k) (Context domain)
              where
                    writer
                            | isPublic k = File.hasPrivContentExposedFrom
                            | otherwise = File.hasPrivContentFrom

            keys = [ PubZSK, PrivZSK, PubKSK, PrivKSK ]

            keysrc k = PrivDataSource (DnsSec k) $ unwords
                    [ "The file with extension"
                    , keyExt k
                    , "created by running:"
                    , if isZoneSigningKey k
                            then "dnssec-keygen -a RSASHA256 -b 2048 -n ZONE " ++ domain
                            else "dnssec-keygen -f KSK -a RSASHA256 -b 4096 -n ZONE " ++ domain
                    ]

    -- | Uses dnssec-signzone to sign a domain's zone file.
    --
    -- signedPrimary uses this, so this property does not normally need to be
    -- used directly.
    zoneSigned :: Domain -> FilePath -> RevertableProperty (HasInfo + UnixLike) UnixLike
    zoneSigned domain zonefile = setup <!> cleanup
      where
            setup :: Property (HasInfo + UnixLike)
            setup = check needupdate (forceZoneSigned domain zonefile)
                    `requires` keysInstalled domain

            cleanup :: Property UnixLike
            cleanup = File.notPresent (signedZoneFile zonefile)
                    `before` File.notPresent dssetfile
                    `before` revert (keysInstalled domain)

            dssetfile = dir </> "-" ++ domain ++ "."
            dir = takeDirectory zonefile

            -- Need to update the signed zone file if the zone file or
            -- any of the keys have a newer timestamp.
            needupdate = do
                    v <- catchMaybeIO $ getModificationTime (signedZoneFile zonefile)
                    case v of
                            Nothing -> return True
                            Just t1 -> anyM (newerthan t1) $
                                    zonefile : map (keyFn domain) [minBound..maxBound]

            newerthan t1 f = do
                    t2 <- getModificationTime f
                    return (t2 >= t1)

    forceZoneSigned :: Domain -> FilePath -> Property UnixLike
    forceZoneSigned domain zonefile = property ("zone signed for " ++ domain) $ liftIO $ do
            salt <- take 16 <$> saltSha1
            let p = proc "dnssec-signzone"
                    [ "-A"
                    , "-3", salt
                    -- The serial number needs to be increased each time the
                    -- zone is resigned, even if there are no other changes,
                    -- so that it will propagate to secondaries. So, use the
                    -- unixtime serial format.
                    , "-N", "unixtime"
                    , "-o", domain
                    , zonefile
                    -- the ordering of these key files does not matter
                    , keyFn domain PubZSK
                    , keyFn domain PubKSK
                    ]
            -- Run in the same directory as the zonefile, so it will
            -- write the dsset file there.
            (_, _, _, h) <- createProcess $
                    p { cwd = Just (takeDirectory zonefile) }
            ifM (checkSuccessProcess h)
                    ( return MadeChange
                    , return FailedChange
                    )

    saltSha1 :: IO String
    saltSha1 = readProcess "sh"
            [ "-c"
            , "head -c 1024 /dev/urandom | sha1sum | cut -d ' ' -f 1"
            ]

    -- | The file used for a given key.
    keyFn :: Domain -> DnsSecKey -> FilePath
    keyFn domain k =  "/etc/bind/propellor/dnssec" </> concat
            [ "K" ++ domain ++ "."
            , if isZoneSigningKey k then "ZSK" else "KSK"
            , keyExt k
            ]

    -- | These are the extensions that dnssec-keygen looks for.
    keyExt :: DnsSecKey -> String
    keyExt k
            | isPublic k = ".key"
            | otherwise = ".private"

    isPublic :: DnsSecKey -> Bool
    isPublic k = k `elem` [PubZSK, PubKSK]

    isZoneSigningKey :: DnsSecKey -> Bool
    isZoneSigningKey k = k `elem` [PubZSK, PrivZSK]

    -- | dnssec-signzone makes a .signed file
    signedZoneFile :: FilePath -> FilePath
    signedZoneFile zonefile = zonefile ++ ".signed"