module Propellor.Gpg where
import System.IO
import System.Posix.IO
import System.Posix.Terminal
import Data.Maybe
import Control.Monad
import Control.Applicative
import Prelude
import Propellor.PrivData.Paths
import Propellor.Message
import Propellor.Git.Config
import Utility.SafeCommand
import Utility.Process
import Utility.Process.Transcript
import Utility.Process.NonConcurrent
import Utility.Monad
import Utility.Misc
import Utility.Tmp
import Utility.Env
import Utility.Env.Set
import Utility.Directory
import Utility.Split
import Utility.Exception
setupGpgEnv :: IO ()
setupGpgEnv :: IO ()
setupGpgEnv = [Fd] -> IO ()
checkhandles [Fd
stdInput, Fd
stdOutput, Fd
stdError]
where
checkhandles :: [Fd] -> IO ()
checkhandles [] = () -> IO ()
forall (m :: * -> *) a. Monad m => a -> m a
return ()
checkhandles (h :: Fd
h:hs :: [Fd]
hs) = do
Bool
isterm <- Fd -> IO Bool
queryTerminal Fd
h
if Bool
isterm
then do
Either SomeException FilePath
v <- IO FilePath -> IO (Either SomeException FilePath)
forall (m :: * -> *) a.
MonadCatch m =>
m a -> m (Either SomeException a)
tryNonAsync (IO FilePath -> IO (Either SomeException FilePath))
-> IO FilePath -> IO (Either SomeException FilePath)
forall a b. (a -> b) -> a -> b
$ Fd -> IO FilePath
getTerminalName Fd
h
case Either SomeException FilePath
v of
Right ttyname :: FilePath
ttyname ->
FilePath -> FilePath -> Bool -> IO ()
setEnv "GPG_TTY" FilePath
ttyname Bool
False
Left _ -> [Fd] -> IO ()
checkhandles [Fd]
hs
else [Fd] -> IO ()
checkhandles [Fd]
hs
type KeyId = String
getGpgBin :: IO String
getGpgBin :: IO FilePath
getGpgBin = do
Maybe FilePath
gitGpgBin <- FilePath -> IO (Maybe FilePath)
getGitConfigValue "gpg.program"
case Maybe FilePath
gitGpgBin of
Nothing -> FilePath -> FilePath -> IO FilePath
getEnvDefault "GNUPGBIN" "gpg"
Just b :: FilePath
b -> FilePath -> IO FilePath
forall (m :: * -> *) a. Monad m => a -> m a
return FilePath
b
listPubKeys :: IO [KeyId]
listPubKeys :: IO [FilePath]
listPubKeys = do
FilePath
keyring <- IO FilePath
privDataKeyring
let listopts :: [FilePath]
listopts =
[ "--list-public-keys"
, "--with-colons"
, "--fixed-list-mode"
] [FilePath] -> [FilePath] -> [FilePath]
forall a. [a] -> [a] -> [a]
++ FilePath -> [FilePath]
useKeyringOpts FilePath
keyring
FilePath
gpgbin <- IO FilePath
getGpgBin
[FilePath] -> [FilePath]
parse ([FilePath] -> [FilePath])
-> (FilePath -> [FilePath]) -> FilePath -> [FilePath]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. FilePath -> [FilePath]
lines (FilePath -> [FilePath]) -> IO FilePath -> IO [FilePath]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> FilePath -> [FilePath] -> IO FilePath
readProcess FilePath
gpgbin [FilePath]
listopts
where
parse :: [FilePath] -> [FilePath]
parse = (FilePath -> Maybe FilePath) -> [FilePath] -> [FilePath]
forall a b. (a -> Maybe b) -> [a] -> [b]
mapMaybe ([FilePath] -> Maybe FilePath
extract ([FilePath] -> Maybe FilePath)
-> (FilePath -> [FilePath]) -> FilePath -> Maybe FilePath
forall b c a. (b -> c) -> (a -> b) -> a -> c
. FilePath -> FilePath -> [FilePath]
forall a. Eq a => [a] -> [a] -> [[a]]
split ":")
extract :: [FilePath] -> Maybe FilePath
extract ("pub":_:_:_:f :: FilePath
f:_) = FilePath -> Maybe FilePath
forall a. a -> Maybe a
Just FilePath
f
extract _ = Maybe FilePath
forall a. Maybe a
Nothing
listSecretKeys :: IO [(KeyId, String)]
listSecretKeys :: IO [(FilePath, FilePath)]
listSecretKeys = do
FilePath
gpgbin <- IO FilePath
getGpgBin
[FilePath] -> [(FilePath, FilePath)]
parse ([FilePath] -> [(FilePath, FilePath)])
-> (FilePath -> [FilePath]) -> FilePath -> [(FilePath, FilePath)]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. FilePath -> [FilePath]
lines (FilePath -> [(FilePath, FilePath)])
-> IO FilePath -> IO [(FilePath, FilePath)]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> FilePath -> [FilePath] -> IO FilePath
readProcess FilePath
gpgbin
[ "--list-secret-keys"
, "--with-colons"
, "--fixed-list-mode"
]
where
parse :: [FilePath] -> [(FilePath, FilePath)]
parse = [(FilePath, FilePath)]
-> Maybe FilePath -> [[FilePath]] -> [(FilePath, FilePath)]
extract [] Maybe FilePath
forall a. Maybe a
Nothing ([[FilePath]] -> [(FilePath, FilePath)])
-> ([FilePath] -> [[FilePath]])
-> [FilePath]
-> [(FilePath, FilePath)]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (FilePath -> [FilePath]) -> [FilePath] -> [[FilePath]]
forall a b. (a -> b) -> [a] -> [b]
map (FilePath -> FilePath -> [FilePath]
forall a. Eq a => [a] -> [a] -> [[a]]
split ":")
extract :: [(FilePath, FilePath)]
-> Maybe FilePath -> [[FilePath]] -> [(FilePath, FilePath)]
extract c :: [(FilePath, FilePath)]
c (Just keyid :: FilePath
keyid) (("uid":_:_:_:_:_:_:_:_:userid :: FilePath
userid:_):rest :: [[FilePath]]
rest) =
[(FilePath, FilePath)]
-> Maybe FilePath -> [[FilePath]] -> [(FilePath, FilePath)]
extract ((FilePath
keyid, FilePath
userid)(FilePath, FilePath)
-> [(FilePath, FilePath)] -> [(FilePath, FilePath)]
forall a. a -> [a] -> [a]
:[(FilePath, FilePath)]
c) Maybe FilePath
forall a. Maybe a
Nothing [[FilePath]]
rest
extract c :: [(FilePath, FilePath)]
c (Just keyid :: FilePath
keyid) rest :: [[FilePath]]
rest@(("sec":_):_) =
[(FilePath, FilePath)]
-> Maybe FilePath -> [[FilePath]] -> [(FilePath, FilePath)]
extract ((FilePath
keyid, "")(FilePath, FilePath)
-> [(FilePath, FilePath)] -> [(FilePath, FilePath)]
forall a. a -> [a] -> [a]
:[(FilePath, FilePath)]
c) Maybe FilePath
forall a. Maybe a
Nothing [[FilePath]]
rest
extract c :: [(FilePath, FilePath)]
c (Just keyid :: FilePath
keyid) rest :: [[FilePath]]
rest@(("pub":_):_) =
[(FilePath, FilePath)]
-> Maybe FilePath -> [[FilePath]] -> [(FilePath, FilePath)]
extract ((FilePath
keyid, "")(FilePath, FilePath)
-> [(FilePath, FilePath)] -> [(FilePath, FilePath)]
forall a. a -> [a] -> [a]
:[(FilePath, FilePath)]
c) Maybe FilePath
forall a. Maybe a
Nothing [[FilePath]]
rest
extract c :: [(FilePath, FilePath)]
c (Just keyid :: FilePath
keyid) (_:rest :: [[FilePath]]
rest) =
[(FilePath, FilePath)]
-> Maybe FilePath -> [[FilePath]] -> [(FilePath, FilePath)]
extract [(FilePath, FilePath)]
c (FilePath -> Maybe FilePath
forall a. a -> Maybe a
Just FilePath
keyid) [[FilePath]]
rest
extract c :: [(FilePath, FilePath)]
c _ [] = [(FilePath, FilePath)]
c
extract c :: [(FilePath, FilePath)]
c _ (("sec":_:_:_:keyid :: FilePath
keyid:_):rest :: [[FilePath]]
rest) =
[(FilePath, FilePath)]
-> Maybe FilePath -> [[FilePath]] -> [(FilePath, FilePath)]
extract [(FilePath, FilePath)]
c (FilePath -> Maybe FilePath
forall a. a -> Maybe a
Just FilePath
keyid) [[FilePath]]
rest
extract c :: [(FilePath, FilePath)]
c k :: Maybe FilePath
k (_:rest :: [[FilePath]]
rest) =
[(FilePath, FilePath)]
-> Maybe FilePath -> [[FilePath]] -> [(FilePath, FilePath)]
extract [(FilePath, FilePath)]
c Maybe FilePath
k [[FilePath]]
rest
useKeyringOpts :: FilePath -> [String]
useKeyringOpts :: FilePath -> [FilePath]
useKeyringOpts keyring :: FilePath
keyring =
[ "--options"
, "/dev/null"
, "--no-default-keyring"
, "--keyring", FilePath
keyring
]
addKey :: KeyId -> IO ()
addKey :: FilePath -> IO ()
addKey keyid :: FilePath
keyid = do
FilePath
gpgbin <- IO FilePath
getGpgBin
FilePath
keyring <- IO FilePath
privDataKeyring
Bool -> IO ()
forall a. Bool -> IO a
exitBool (Bool -> IO ()) -> IO Bool -> IO ()
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< ((FilePath, IO Bool) -> IO Bool)
-> [(FilePath, IO Bool)] -> IO Bool
forall (m :: * -> *) a. Monad m => (a -> m Bool) -> [a] -> m Bool
allM ((FilePath -> IO Bool -> IO Bool) -> (FilePath, IO Bool) -> IO Bool
forall a b c. (a -> b -> c) -> (a, b) -> c
uncurry FilePath -> IO Bool -> IO Bool
forall (m :: * -> *) r.
(MonadIO m, MonadMask m, ActionResult r, ToResult r) =>
FilePath -> m r -> m r
actionMessage)
[ ("adding key to propellor's keyring", FilePath -> FilePath -> IO Bool
addkeyring FilePath
keyring FilePath
gpgbin)
, ("staging propellor's keyring", FilePath -> IO Bool
gitAdd FilePath
keyring)
, ("updating encryption of any privdata", IO Bool
reencryptPrivData)
, ("configuring git commit signing to use key", FilePath -> IO Bool
gitconfig FilePath
gpgbin)
, ("committing changes", FilePath -> IO Bool
gitCommitKeyRing "add-key")
]
where
addkeyring :: FilePath -> FilePath -> IO Bool
addkeyring keyring' :: FilePath
keyring' gpgbin' :: FilePath
gpgbin' = do
Bool -> FilePath -> IO ()
createDirectoryIfMissing Bool
True FilePath
privDataDir
FilePath -> [CommandParam] -> IO Bool
boolSystem "sh"
[ FilePath -> CommandParam
Param "-c"
, FilePath -> CommandParam
Param (FilePath -> CommandParam) -> FilePath -> CommandParam
forall a b. (a -> b) -> a -> b
$ FilePath
gpgbin' FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ " --export " FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ FilePath
keyid FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ " | gpg " FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++
[FilePath] -> FilePath
unwords (FilePath -> [FilePath]
useKeyringOpts FilePath
keyring' [FilePath] -> [FilePath] -> [FilePath]
forall a. [a] -> [a] -> [a]
++ ["--import"])
]
gitconfig :: FilePath -> IO Bool
gitconfig gpgbin' :: FilePath
gpgbin' = IO Bool -> (IO Bool, IO Bool) -> IO Bool
forall (m :: * -> *) a. Monad m => m Bool -> (m a, m a) -> m a
ifM ((FilePath, Bool) -> Bool
forall a b. (a, b) -> b
snd ((FilePath, Bool) -> Bool) -> IO (FilePath, Bool) -> IO Bool
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> FilePath -> [FilePath] -> Maybe FilePath -> IO (FilePath, Bool)
processTranscript FilePath
gpgbin' ["--list-secret-keys", FilePath
keyid] Maybe FilePath
forall a. Maybe a
Nothing)
( FilePath -> [CommandParam] -> IO Bool
boolSystem "git"
[ FilePath -> CommandParam
Param "config"
, FilePath -> CommandParam
Param "user.signingkey"
, FilePath -> CommandParam
Param FilePath
keyid
]
, do
FilePath -> IO ()
forall (m :: * -> *). MonadIO m => FilePath -> m ()
warningMessage (FilePath -> IO ()) -> FilePath -> IO ()
forall a b. (a -> b) -> a -> b
$ "Cannot find a secret key for key " FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ FilePath
keyid FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ ", so not configuring git user.signingkey to use this key."
Bool -> IO Bool
forall (m :: * -> *) a. Monad m => a -> m a
return Bool
True
)
rmKey :: KeyId -> IO ()
rmKey :: FilePath -> IO ()
rmKey keyid :: FilePath
keyid = do
FilePath
gpgbin <- IO FilePath
getGpgBin
FilePath
keyring <- IO FilePath
privDataKeyring
Bool -> IO ()
forall a. Bool -> IO a
exitBool (Bool -> IO ()) -> IO Bool -> IO ()
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< ((FilePath, IO Bool) -> IO Bool)
-> [(FilePath, IO Bool)] -> IO Bool
forall (m :: * -> *) a. Monad m => (a -> m Bool) -> [a] -> m Bool
allM ((FilePath -> IO Bool -> IO Bool) -> (FilePath, IO Bool) -> IO Bool
forall a b c. (a -> b -> c) -> (a, b) -> c
uncurry FilePath -> IO Bool -> IO Bool
forall (m :: * -> *) r.
(MonadIO m, MonadMask m, ActionResult r, ToResult r) =>
FilePath -> m r -> m r
actionMessage)
[ ("removing key from propellor's keyring", FilePath -> FilePath -> IO Bool
rmkeyring FilePath
keyring FilePath
gpgbin)
, ("staging propellor's keyring", FilePath -> IO Bool
gitAdd FilePath
keyring)
, ("updating encryption of any privdata", IO Bool
reencryptPrivData)
, ("configuring git commit signing to not use key", IO Bool
gitconfig)
, ("committing changes", FilePath -> IO Bool
gitCommitKeyRing "rm-key")
]
where
rmkeyring :: FilePath -> FilePath -> IO Bool
rmkeyring keyring' :: FilePath
keyring' gpgbin' :: FilePath
gpgbin' = FilePath -> [CommandParam] -> IO Bool
boolSystem FilePath
gpgbin' ([CommandParam] -> IO Bool) -> [CommandParam] -> IO Bool
forall a b. (a -> b) -> a -> b
$
((FilePath -> CommandParam) -> [FilePath] -> [CommandParam]
forall a b. (a -> b) -> [a] -> [b]
map FilePath -> CommandParam
Param (FilePath -> [FilePath]
useKeyringOpts FilePath
keyring')) [CommandParam] -> [CommandParam] -> [CommandParam]
forall a. [a] -> [a] -> [a]
++
[ FilePath -> CommandParam
Param "--batch"
, FilePath -> CommandParam
Param "--yes"
, FilePath -> CommandParam
Param "--delete-key", FilePath -> CommandParam
Param FilePath
keyid
]
gitconfig :: IO Bool
gitconfig = IO Bool -> (IO Bool, IO Bool) -> IO Bool
forall (m :: * -> *) a. Monad m => m Bool -> (m a, m a) -> m a
ifM ((FilePath, Bool) -> (FilePath, Bool) -> Bool
forall a. Eq a => a -> a -> Bool
(==) (FilePath
keyidFilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++"\n", Bool
True) ((FilePath, Bool) -> Bool) -> IO (FilePath, Bool) -> IO Bool
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> FilePath -> [FilePath] -> Maybe FilePath -> IO (FilePath, Bool)
processTranscript "git" ["config", "user.signingkey"] Maybe FilePath
forall a. Maybe a
Nothing)
( FilePath -> [CommandParam] -> IO Bool
boolSystem "git"
[ FilePath -> CommandParam
Param "config"
, FilePath -> CommandParam
Param "--unset"
, FilePath -> CommandParam
Param "user.signingkey"
]
, Bool -> IO Bool
forall (m :: * -> *) a. Monad m => a -> m a
return Bool
True
)
reencryptPrivData :: IO Bool
reencryptPrivData :: IO Bool
reencryptPrivData = do
FilePath
f <- IO FilePath
privDataFile
IO Bool -> (IO Bool, IO Bool) -> IO Bool
forall (m :: * -> *) a. Monad m => m Bool -> (m a, m a) -> m a
ifM (FilePath -> IO Bool
doesFileExist FilePath
f)
( do
FilePath -> FilePath -> IO ()
gpgEncrypt FilePath
f (FilePath -> IO ()) -> IO FilePath -> IO ()
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< FilePath -> IO FilePath
gpgDecrypt FilePath
f
FilePath -> IO Bool
gitAdd FilePath
f
, Bool -> IO Bool
forall (m :: * -> *) a. Monad m => a -> m a
return Bool
True
)
gitAdd :: FilePath -> IO Bool
gitAdd :: FilePath -> IO Bool
gitAdd f :: FilePath
f = FilePath -> [CommandParam] -> IO Bool
boolSystem "git"
[ FilePath -> CommandParam
Param "add"
, FilePath -> CommandParam
File FilePath
f
]
gitCommitKeyRing :: String -> IO Bool
gitCommitKeyRing :: FilePath -> IO Bool
gitCommitKeyRing action :: FilePath
action = do
FilePath
keyring <- IO FilePath
privDataKeyring
FilePath
privdata <- IO FilePath
privDataFile
[FilePath]
tocommit <- (FilePath -> IO Bool) -> [FilePath] -> IO [FilePath]
forall (m :: * -> *) a.
Applicative m =>
(a -> m Bool) -> [a] -> m [a]
filterM FilePath -> IO Bool
doesFileExist [ FilePath
privdata, FilePath
keyring]
Maybe FilePath -> [CommandParam] -> IO Bool
gitCommit (FilePath -> Maybe FilePath
forall a. a -> Maybe a
Just ("propellor " FilePath -> FilePath -> FilePath
forall a. [a] -> [a] -> [a]
++ FilePath
action)) ((FilePath -> CommandParam) -> [FilePath] -> [CommandParam]
forall a b. (a -> b) -> [a] -> [b]
map FilePath -> CommandParam
File [FilePath]
tocommit)
gpgSignParams :: [CommandParam] -> IO [CommandParam]
gpgSignParams :: [CommandParam] -> IO [CommandParam]
gpgSignParams ps :: [CommandParam]
ps = do
FilePath
keyring <- IO FilePath
privDataKeyring
IO Bool
-> (IO [CommandParam], IO [CommandParam]) -> IO [CommandParam]
forall (m :: * -> *) a. Monad m => m Bool -> (m a, m a) -> m a
ifM (FilePath -> IO Bool
doesFileExist FilePath
keyring)
( [CommandParam] -> IO [CommandParam]
forall (m :: * -> *) a. Monad m => a -> m a
return ([CommandParam]
ps [CommandParam] -> [CommandParam] -> [CommandParam]
forall a. [a] -> [a] -> [a]
++ [FilePath -> CommandParam
Param "--gpg-sign"])
, [CommandParam] -> IO [CommandParam]
forall (m :: * -> *) a. Monad m => a -> m a
return [CommandParam]
ps
)
gitCommit :: Maybe String -> [CommandParam] -> IO Bool
gitCommit :: Maybe FilePath -> [CommandParam] -> IO Bool
gitCommit msg :: Maybe FilePath
msg ps :: [CommandParam]
ps = do
let ps' :: [CommandParam]
ps' = FilePath -> CommandParam
Param "commit" CommandParam -> [CommandParam] -> [CommandParam]
forall a. a -> [a] -> [a]
: [CommandParam]
ps [CommandParam] -> [CommandParam] -> [CommandParam]
forall a. [a] -> [a] -> [a]
++
[CommandParam]
-> (FilePath -> [CommandParam]) -> Maybe FilePath -> [CommandParam]
forall b a. b -> (a -> b) -> Maybe a -> b
maybe [] (\m :: FilePath
m -> [FilePath -> CommandParam
Param "-m", FilePath -> CommandParam
Param FilePath
m]) Maybe FilePath
msg
[CommandParam]
ps'' <- [CommandParam] -> IO [CommandParam]
gpgSignParams [CommandParam]
ps'
FilePath -> [CommandParam] -> IO Bool
boolSystemNonConcurrent "git" [CommandParam]
ps''
gpgDecrypt :: FilePath -> IO String
gpgDecrypt :: FilePath -> IO FilePath
gpgDecrypt f :: FilePath
f = do
FilePath
gpgbin <- IO FilePath
getGpgBin
IO Bool -> (IO FilePath, IO FilePath) -> IO FilePath
forall (m :: * -> *) a. Monad m => m Bool -> (m a, m a) -> m a
ifM (FilePath -> IO Bool
doesFileExist FilePath
f)
( FilePath
-> [FilePath]
-> Maybe [(FilePath, FilePath)]
-> Maybe (Handle -> IO ())
-> Maybe (Handle -> IO ())
-> IO FilePath
writeReadProcessEnv FilePath
gpgbin ["--decrypt", FilePath
f] Maybe [(FilePath, FilePath)]
forall a. Maybe a
Nothing Maybe (Handle -> IO ())
forall a. Maybe a
Nothing Maybe (Handle -> IO ())
forall a. Maybe a
Nothing
, FilePath -> IO FilePath
forall (m :: * -> *) a. Monad m => a -> m a
return ""
)
gpgEncrypt :: FilePath -> String -> IO ()
gpgEncrypt :: FilePath -> FilePath -> IO ()
gpgEncrypt f :: FilePath
f s :: FilePath
s = do
FilePath
gpgbin <- IO FilePath
getGpgBin
[FilePath]
keyids <- IO [FilePath]
listPubKeys
let opts :: [FilePath]
opts =
[ "--default-recipient-self"
, "--armor"
, "--encrypt"
, "--trust-model", "always"
] [FilePath] -> [FilePath] -> [FilePath]
forall a. [a] -> [a] -> [a]
++ (FilePath -> [FilePath]) -> [FilePath] -> [FilePath]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (\k :: FilePath
k -> ["--recipient", FilePath
k]) [FilePath]
keyids
FilePath
encrypted <- FilePath
-> [FilePath]
-> Maybe [(FilePath, FilePath)]
-> Maybe (Handle -> IO ())
-> Maybe (Handle -> IO ())
-> IO FilePath
writeReadProcessEnv FilePath
gpgbin [FilePath]
opts Maybe [(FilePath, FilePath)]
forall a. Maybe a
Nothing ((Handle -> IO ()) -> Maybe (Handle -> IO ())
forall a. a -> Maybe a
Just Handle -> IO ()
writer) Maybe (Handle -> IO ())
forall a. Maybe a
Nothing
(FilePath -> FilePath -> IO ()) -> FilePath -> FilePath -> IO ()
forall (m :: * -> *) v.
(MonadMask m, MonadIO m) =>
(FilePath -> v -> m ()) -> FilePath -> v -> m ()
viaTmp FilePath -> FilePath -> IO ()
writeFile FilePath
f FilePath
encrypted
where
writer :: Handle -> IO ()
writer h :: Handle
h = Handle -> FilePath -> IO ()
hPutStr Handle
h FilePath
s