git-annex/Assistant/Ssh.hs
Joey Hess 74906ed13f UI for enabling special remotes
Now other repositories can configure special remotes, and when their
configuration has propigated out, they'll appear in the webapp's list of
repositories, with a link to enable them.

Added support for enabling rsync special remotes, and directory special
remotes that are on removable drives. However, encrypted directory special
remotes are not supported yet. The removable drive configuator doesn't
support them yet anyway.
2012-09-13 16:50:02 -04:00

210 lines
6.2 KiB
Haskell

{- git-annex assistant ssh utilities
-
- Copyright 2012 Joey Hess <joey@kitenet.net>
-
- Licensed under the GNU GPL version 3 or higher.
-}
module Assistant.Ssh where
import Common.Annex
import Utility.TempFile
import Data.Text (Text)
import qualified Data.Text as T
import qualified Control.Exception as E
import System.Process (CreateProcess(..))
import Control.Concurrent
import Data.Char
data SshData = SshData
{ sshHostName :: Text
, sshUserName :: Maybe Text
, sshDirectory :: Text
, sshRepoName :: String
, needsPubKey :: Bool
, rsyncOnly :: Bool
}
deriving (Read, Show, Eq)
data SshKeyPair = SshKeyPair
{ sshPubKey :: String
, sshPrivKey :: String
}
instance Show SshKeyPair where
show = sshPubKey
type SshPubKey = String
{- ssh -ofoo=bar command-line option -}
sshOpt :: String -> String -> String
sshOpt k v = concat ["-o", k, "=", v]
sshDir :: IO FilePath
sshDir = do
home <- myHomeDir
return $ home </> ".ssh"
{- user@host or host -}
genSshHost :: Text -> Maybe Text -> String
genSshHost host user = maybe "" (\v -> T.unpack v ++ "@") user ++ T.unpack host
{- host_dir, with all / in dir replaced by _, and bad characters removed -}
genSshRepoName :: String -> FilePath -> String
genSshRepoName host dir
| null dir = filter legal host
| otherwise = filter legal $ host ++ "_" ++ replace "/" "_" dir
where
legal '_' = True
legal c = isAlphaNum c
{- The output of ssh, including both stdout and stderr. -}
sshTranscript :: [String] -> String -> IO (String, Bool)
sshTranscript opts input = do
(readf, writef) <- createPipe
readh <- fdToHandle readf
writeh <- fdToHandle writef
(Just inh, _, _, pid) <- createProcess $
(proc "ssh" opts)
{ std_in = CreatePipe
, std_out = UseHandle writeh
, std_err = UseHandle writeh
}
hClose writeh
-- fork off a thread to start consuming the output
transcript <- hGetContents readh
outMVar <- newEmptyMVar
_ <- forkIO $ E.evaluate (length transcript) >> putMVar outMVar ()
-- now write and flush any input
unless (null input) $ do
hPutStr inh input
hFlush inh
hClose inh -- done with stdin
-- wait on the output
takeMVar outMVar
hClose readh
ok <- checkSuccessProcess pid
return ()
return (transcript, ok)
{- Ensure that the ssh public key doesn't include any ssh options, like
- command=foo, or other weirdness -}
validateSshPubKey :: SshPubKey -> IO ()
validateSshPubKey pubkey = do
let ws = words pubkey
when (length ws > 3 || length ws < 2) $
error $ "wrong number of words in ssh public key " ++ pubkey
let (ssh, keytype) = separate (== '-') (ws !! 0)
unless (ssh == "ssh" && all isAlphaNum keytype) $
error $ "bad ssh public key prefix " ++ ws !! 0
when (length ws == 3) $
unless (all (\c -> isAlphaNum c || c == '@') (ws !! 2)) $
error $ "bad comment in ssh public key " ++ pubkey
addAuthorizedKeys :: Bool -> SshPubKey -> IO Bool
addAuthorizedKeys rsynconly pubkey = boolSystem "sh"
[ Param "-c" , Param $ addAuthorizedKeysCommand rsynconly pubkey ]
removeAuthorizedKeys :: Bool -> SshPubKey -> IO ()
removeAuthorizedKeys rsynconly pubkey = do
let keyline = authorizedKeysLine rsynconly pubkey
sshdir <- sshDir
let keyfile = sshdir </> ".authorized_keys"
ls <- lines <$> readFileStrict keyfile
writeFile keyfile $ unlines $ filter (/= keyline) ls
{- Implemented as a shell command, so it can be run on remote servers over
- ssh. -}
addAuthorizedKeysCommand :: Bool -> SshPubKey -> String
addAuthorizedKeysCommand rsynconly pubkey = join "&&"
[ "mkdir -p ~/.ssh"
, "touch ~/.ssh/authorized_keys"
, "chmod 600 ~/.ssh/authorized_keys"
, unwords
[ "echo"
, shellEscape $ authorizedKeysLine rsynconly pubkey
, ">>~/.ssh/authorized_keys"
]
]
authorizedKeysLine :: Bool -> SshPubKey -> String
authorizedKeysLine rsynconly pubkey
{- TODO: Locking down rsync is difficult, requiring a rather
- long perl script. -}
| rsynconly = pubkey
| otherwise = limitcommand "git-annex-shell -c" ++ pubkey
where
limitcommand c = "command=\"perl -e 'exec qw(" ++ c ++ "), $ENV{SSH_ORIGINAL_COMMAND}'\",no-agent-forwarding,no-port-forwarding,no-X11-forwarding "
{- Generates a ssh key pair. -}
genSshKeyPair :: IO SshKeyPair
genSshKeyPair = withTempDir "git-annex-keygen" $ \dir -> do
ok <- boolSystem "ssh-keygen"
[ Param "-P", Param "" -- no password
, Param "-f", File $ dir </> "key"
]
unless ok $
error "ssh-keygen failed"
SshKeyPair
<$> readFile (dir </> "key.pub")
<*> readFile (dir </> "key")
{- Installs a ssh key pair, and sets up ssh config with a mangled hostname
- that will enable use of the key. This way we avoid changing the user's
- regular ssh experience at all. Returns a modified SshData containing the
- mangled hostname. -}
setupSshKeyPair :: SshKeyPair -> SshData -> IO SshData
setupSshKeyPair sshkeypair sshdata = do
sshdir <- sshDir
let configfile = sshdir </> "config"
createDirectoryIfMissing True sshdir
unlessM (doesFileExist $ sshdir </> sshprivkeyfile) $ do
h <- fdToHandle =<<
createFile (sshdir </> sshprivkeyfile)
(unionFileModes ownerWriteMode ownerReadMode)
hPutStr h (sshPrivKey sshkeypair)
hClose h
unlessM (doesFileExist $ sshdir </> sshpubkeyfile) $
writeFile (sshdir </> sshpubkeyfile) (sshPubKey sshkeypair)
unlessM (catchBoolIO $ isInfixOf mangledhost <$> readFile configfile) $
appendFile configfile $ unlines
[ ""
, "# Added automatically by git-annex"
, "Host " ++ mangledhost
, "\tHostname " ++ T.unpack (sshHostName sshdata)
, "\tIdentityFile ~/.ssh/" ++ sshprivkeyfile
]
return $ sshdata { sshHostName = T.pack mangledhost }
where
sshprivkeyfile = "key." ++ mangledhost
sshpubkeyfile = sshprivkeyfile ++ ".pub"
mangledhost = mangleSshHostName
(T.unpack $ sshHostName sshdata)
(T.unpack <$> sshUserName sshdata)
mangleSshHostName :: String -> Maybe String -> String
mangleSshHostName host user = "git-annex-" ++ host ++ (maybe "-" ('-':) user)
unMangleSshHostName :: String -> String
unMangleSshHostName h
| "git-annex-" `isPrefixOf` h = join "-" (beginning $ drop 2 dashbits)
| otherwise = h
where
dashbits = split "-" h
{- Does ssh have known_hosts data for a hostname? -}
knownHost :: Text -> IO Bool
knownHost hostname = do
sshdir <- sshDir
ifM (doesFileExist $ sshdir </> "known_hosts")
( not . null <$> readProcess "ssh-keygen" ["-F", T.unpack hostname]
, return False
)