2012-01-20 19:34:52 +00:00
|
|
|
{- git-annex ssh interface, with connection caching
|
|
|
|
-
|
2015-02-12 19:44:10 +00:00
|
|
|
- Copyright 2012-2015 Joey Hess <id@joeyh.name>
|
2012-01-20 19:34:52 +00:00
|
|
|
-
|
|
|
|
- Licensed under the GNU GPL version 3 or higher.
|
|
|
|
-}
|
|
|
|
|
2012-10-15 18:49:40 +00:00
|
|
|
{-# LANGUAGE CPP #-}
|
|
|
|
|
2012-01-20 19:34:52 +00:00
|
|
|
module Annex.Ssh (
|
2015-02-12 19:44:10 +00:00
|
|
|
sshOptions,
|
2013-05-14 17:53:29 +00:00
|
|
|
sshCacheDir,
|
2013-04-13 22:10:49 +00:00
|
|
|
sshReadPort,
|
2014-04-12 20:32:59 +00:00
|
|
|
forceSshCleanup,
|
2015-02-12 20:12:32 +00:00
|
|
|
sshOptionsEnv,
|
|
|
|
sshOptionsTo,
|
|
|
|
inRepoWithSshOptionsTo,
|
|
|
|
runSshOptions,
|
2014-04-29 22:08:10 +00:00
|
|
|
sshAskPassEnv,
|
|
|
|
runSshAskPass
|
2012-01-20 19:34:52 +00:00
|
|
|
) where
|
|
|
|
|
|
|
|
import qualified Data.Map as M
|
2013-07-22 19:06:47 +00:00
|
|
|
import Data.Hash.MD5
|
2014-04-12 19:59:34 +00:00
|
|
|
import System.Exit
|
2012-01-20 19:34:52 +00:00
|
|
|
|
2016-01-20 20:36:33 +00:00
|
|
|
import Annex.Common
|
2014-07-10 04:32:23 +00:00
|
|
|
import Annex.LockFile
|
2012-02-25 23:15:29 +00:00
|
|
|
import qualified Build.SysConfig as SysConfig
|
2012-12-30 03:10:18 +00:00
|
|
|
import qualified Annex
|
2014-04-12 19:59:34 +00:00
|
|
|
import qualified Git
|
|
|
|
import qualified Git.Url
|
2013-02-19 18:56:24 +00:00
|
|
|
import Config
|
2015-02-28 21:23:13 +00:00
|
|
|
import Annex.Path
|
2013-05-11 22:23:41 +00:00
|
|
|
import Utility.Env
|
2016-10-26 20:41:34 +00:00
|
|
|
import Utility.Tmp
|
2014-03-13 23:30:13 +00:00
|
|
|
import Types.CleanupActions
|
2016-02-25 17:46:31 +00:00
|
|
|
import Git.Env
|
2013-08-04 17:12:18 +00:00
|
|
|
#ifndef mingw32_HOST_OS
|
|
|
|
import Annex.Perms
|
2015-11-12 22:05:45 +00:00
|
|
|
import Annex.LockPool
|
2013-08-04 17:12:18 +00:00
|
|
|
#endif
|
2012-01-20 19:34:52 +00:00
|
|
|
|
|
|
|
{- Generates parameters to ssh to a given host (or user@host) on a given
|
2015-02-12 19:44:10 +00:00
|
|
|
- port. This includes connection caching parameters, and any ssh-options. -}
|
|
|
|
sshOptions :: (String, Maybe Integer) -> RemoteGitConfig -> [CommandParam] -> Annex [CommandParam]
|
|
|
|
sshOptions (host, port) gc opts = go =<< sshCachingInfo (host, port)
|
2012-12-13 04:24:19 +00:00
|
|
|
where
|
|
|
|
go (Nothing, params) = ret params
|
|
|
|
go (Just socketfile, params) = do
|
2014-04-12 19:59:34 +00:00
|
|
|
prepSocket socketfile
|
2012-12-13 04:24:19 +00:00
|
|
|
ret params
|
2016-10-26 20:41:34 +00:00
|
|
|
ret ps = do
|
|
|
|
overideconfigfile <- fromRepo gitAnnexSshConfig
|
|
|
|
-- We assume that the file content does not change.
|
|
|
|
-- If it did, a more expensive test would be needed.
|
|
|
|
liftIO $ unlessM (doesFileExist overideconfigfile) $
|
|
|
|
viaTmp writeFile overideconfigfile $ unlines
|
|
|
|
-- ssh expands "~"
|
|
|
|
[ "Include ~/.ssh/config"
|
|
|
|
-- ssh will silently skip the file
|
|
|
|
-- if it does not exist
|
|
|
|
, "Include /etc/ssh/ssh_config"
|
|
|
|
-- Everything below this point is only
|
|
|
|
-- used if there's no setting for it in
|
|
|
|
-- the above files.
|
|
|
|
--
|
|
|
|
-- Make sure that ssh detects stalled
|
|
|
|
-- connections.
|
|
|
|
, "ServerAliveInterval 60"
|
|
|
|
]
|
|
|
|
return $ concat
|
|
|
|
[ ps
|
|
|
|
, [Param "-F", File overideconfigfile]
|
|
|
|
, map Param (remoteAnnexSshOptions gc)
|
|
|
|
, opts
|
|
|
|
, portParams port
|
|
|
|
, [Param "-T"]
|
|
|
|
]
|
2012-01-20 19:34:52 +00:00
|
|
|
|
2013-02-19 18:56:24 +00:00
|
|
|
{- Returns a filename to use for a ssh connection caching socket, and
|
|
|
|
- parameters to enable ssh connection caching. -}
|
2015-02-12 19:44:10 +00:00
|
|
|
sshCachingInfo :: (String, Maybe Integer) -> Annex (Maybe FilePath, [CommandParam])
|
|
|
|
sshCachingInfo (host, port) = go =<< sshCacheDir
|
2013-02-19 18:56:24 +00:00
|
|
|
where
|
|
|
|
go Nothing = return (Nothing, [])
|
|
|
|
go (Just dir) = do
|
2013-10-07 00:43:22 +00:00
|
|
|
r <- liftIO $ bestSocketPath $ dir </> hostport2socket host port
|
|
|
|
return $ case r of
|
|
|
|
Nothing -> (Nothing, [])
|
|
|
|
Just socketfile -> (Just socketfile, sshConnectionCachingParams socketfile)
|
|
|
|
|
|
|
|
{- Given an absolute path to use for a socket file,
|
|
|
|
- returns whichever is shorter of that or the relative path to the same
|
|
|
|
- file.
|
|
|
|
-
|
|
|
|
- If no path can be constructed that is a valid socket, returns Nothing. -}
|
|
|
|
bestSocketPath :: FilePath -> IO (Maybe FilePath)
|
|
|
|
bestSocketPath abssocketfile = do
|
|
|
|
relsocketfile <- liftIO $ relPathCwdToFile abssocketfile
|
|
|
|
let socketfile = if length abssocketfile <= length relsocketfile
|
|
|
|
then abssocketfile
|
|
|
|
else relsocketfile
|
|
|
|
return $ if valid_unix_socket_path (socketfile ++ sshgarbage)
|
|
|
|
then Just socketfile
|
|
|
|
else Nothing
|
|
|
|
where
|
2014-10-09 18:53:13 +00:00
|
|
|
-- ssh appends a 16 char extension to the socket when setting it
|
2013-10-07 00:43:22 +00:00
|
|
|
-- up, which needs to be taken into account when checking
|
|
|
|
-- that a valid socket was constructed.
|
2014-10-09 18:53:13 +00:00
|
|
|
sshgarbage = replicate (1+16) 'X'
|
2013-07-21 18:14:54 +00:00
|
|
|
|
|
|
|
sshConnectionCachingParams :: FilePath -> [CommandParam]
|
|
|
|
sshConnectionCachingParams socketfile =
|
|
|
|
[ Param "-S", Param socketfile
|
2015-06-01 17:52:23 +00:00
|
|
|
, Param "-o", Param "ControlMaster=auto"
|
|
|
|
, Param "-o", Param "ControlPersist=yes"
|
2013-07-21 18:14:54 +00:00
|
|
|
]
|
2012-01-20 19:34:52 +00:00
|
|
|
|
2013-06-18 02:13:28 +00:00
|
|
|
{- ssh connection caching creates sockets, so will not work on a
|
|
|
|
- crippled filesystem. A GIT_ANNEX_TMP_DIR can be provided to use
|
2013-02-19 18:56:24 +00:00
|
|
|
- a different filesystem. -}
|
|
|
|
sshCacheDir :: Annex (Maybe FilePath)
|
|
|
|
sshCacheDir
|
2016-04-20 18:43:43 +00:00
|
|
|
| SysConfig.sshconnectioncaching =
|
|
|
|
ifM (fromMaybe True . annexSshCaching <$> Annex.getGitConfig)
|
|
|
|
( ifM crippledFileSystem
|
|
|
|
( maybe (return Nothing) usetmpdir =<< gettmpdir
|
|
|
|
, Just <$> fromRepo gitAnnexSshDir
|
|
|
|
)
|
2013-06-18 02:13:28 +00:00
|
|
|
, return Nothing
|
2013-02-19 18:56:24 +00:00
|
|
|
)
|
|
|
|
| otherwise = return Nothing
|
|
|
|
where
|
|
|
|
gettmpdir = liftIO $ getEnv "GIT_ANNEX_TMP_DIR"
|
2013-02-19 21:31:08 +00:00
|
|
|
usetmpdir tmpdir = liftIO $ catchMaybeIO $ do
|
2014-04-20 20:56:01 +00:00
|
|
|
let socktmp = tmpdir </> "ssh"
|
|
|
|
createDirectoryIfMissing True socktmp
|
|
|
|
return socktmp
|
2012-01-20 19:34:52 +00:00
|
|
|
|
|
|
|
portParams :: Maybe Integer -> [CommandParam]
|
|
|
|
portParams Nothing = []
|
|
|
|
portParams (Just port) = [Param "-p", Param $ show port]
|
|
|
|
|
2014-04-12 19:59:34 +00:00
|
|
|
{- Prepare to use a socket file. Locks a lock file to prevent
|
|
|
|
- other git-annex processes from stopping the ssh on this socket. -}
|
|
|
|
prepSocket :: FilePath -> Annex ()
|
|
|
|
prepSocket socketfile = do
|
|
|
|
-- If the lock pool is empty, this is the first ssh of this
|
|
|
|
-- run. There could be stale ssh connections hanging around
|
|
|
|
-- from a previous git-annex run that was interrupted.
|
2015-05-18 20:23:07 +00:00
|
|
|
whenM (not . any isLock . M.keys <$> getLockCache)
|
2014-04-12 19:59:34 +00:00
|
|
|
sshCleanup
|
|
|
|
-- Cleanup at end of this run.
|
|
|
|
Annex.addCleanup SshCachingCleanup sshCleanup
|
|
|
|
|
2015-01-09 17:11:56 +00:00
|
|
|
liftIO $ createDirectoryIfMissing True $ parentDir socketfile
|
2015-05-18 20:23:07 +00:00
|
|
|
lockFileCached $ socket2lock socketfile
|
2014-04-12 19:59:34 +00:00
|
|
|
|
2016-10-26 17:16:41 +00:00
|
|
|
{- Find ssh socket files.
|
|
|
|
-
|
|
|
|
- The check that the lock file exists makes only socket files
|
|
|
|
- that were set up by prepSocket be found. On some NFS systems,
|
|
|
|
- a deleted socket file may linger for a while under another filename;
|
|
|
|
- and this check makes such files be skipped since the corresponding lock
|
|
|
|
- file won't exist.
|
|
|
|
-}
|
2014-04-12 20:32:59 +00:00
|
|
|
enumSocketFiles :: Annex [FilePath]
|
2016-10-26 17:16:41 +00:00
|
|
|
enumSocketFiles = liftIO . go =<< sshCacheDir
|
2014-04-12 20:32:59 +00:00
|
|
|
where
|
|
|
|
go Nothing = return []
|
2016-10-26 17:16:41 +00:00
|
|
|
go (Just dir) = filterM (doesFileExist . socket2lock)
|
|
|
|
=<< filter (not . isLock)
|
2014-04-12 20:32:59 +00:00
|
|
|
<$> catchDefaultIO [] (dirContents dir)
|
|
|
|
|
|
|
|
{- Stop any unused ssh connection caching processes. -}
|
2012-01-20 19:34:52 +00:00
|
|
|
sshCleanup :: Annex ()
|
2014-04-12 20:32:59 +00:00
|
|
|
sshCleanup = mapM_ cleanup =<< enumSocketFiles
|
2012-12-13 04:24:19 +00:00
|
|
|
where
|
|
|
|
cleanup socketfile = do
|
2013-08-02 16:27:32 +00:00
|
|
|
#ifndef mingw32_HOST_OS
|
2012-12-13 04:24:19 +00:00
|
|
|
-- Drop any shared lock we have, and take an
|
|
|
|
-- exclusive lock, without blocking. If the lock
|
|
|
|
-- succeeds, nothing is using this ssh, and it can
|
|
|
|
-- be stopped.
|
2014-04-12 20:32:59 +00:00
|
|
|
--
|
|
|
|
-- After ssh is stopped cannot remove the lock file;
|
|
|
|
-- other processes may be waiting on our exclusive
|
|
|
|
-- lock to use it.
|
2012-12-13 04:24:19 +00:00
|
|
|
let lockfile = socket2lock socketfile
|
|
|
|
unlockFile lockfile
|
|
|
|
mode <- annexFileMode
|
2015-11-12 22:05:45 +00:00
|
|
|
v <- noUmask mode $ tryLockExclusive (Just mode) lockfile
|
2012-12-13 04:24:19 +00:00
|
|
|
case v of
|
2014-08-20 22:56:25 +00:00
|
|
|
Nothing -> noop
|
|
|
|
Just lck -> do
|
|
|
|
forceStopSsh socketfile
|
|
|
|
liftIO $ dropLock lck
|
2013-05-11 20:03:00 +00:00
|
|
|
#else
|
2014-04-12 20:32:59 +00:00
|
|
|
forceStopSsh socketfile
|
2013-05-11 20:03:00 +00:00
|
|
|
#endif
|
2014-04-12 20:32:59 +00:00
|
|
|
|
|
|
|
{- Stop all ssh connection caching processes, even when they're in use. -}
|
|
|
|
forceSshCleanup :: Annex ()
|
|
|
|
forceSshCleanup = mapM_ forceStopSsh =<< enumSocketFiles
|
|
|
|
|
|
|
|
forceStopSsh :: FilePath -> Annex ()
|
|
|
|
forceStopSsh socketfile = do
|
|
|
|
let (dir, base) = splitFileName socketfile
|
|
|
|
let params = sshConnectionCachingParams base
|
|
|
|
-- "ssh -O stop" is noisy on stderr even with -q
|
|
|
|
void $ liftIO $ catchMaybeIO $
|
|
|
|
withQuietOutput createProcessSuccess $
|
|
|
|
(proc "ssh" $ toCommand $
|
2015-06-01 17:52:23 +00:00
|
|
|
[ Param "-O", Param "stop" ] ++
|
|
|
|
params ++ [Param "localhost"])
|
2014-04-12 20:32:59 +00:00
|
|
|
{ cwd = Just dir }
|
|
|
|
liftIO $ nukeFile socketfile
|
2012-01-20 19:34:52 +00:00
|
|
|
|
2013-07-22 19:06:47 +00:00
|
|
|
{- This needs to be as short as possible, due to limitations on the length
|
|
|
|
- of the path to a socket file. At the same time, it needs to be unique
|
|
|
|
- for each host.
|
|
|
|
-}
|
2012-01-20 19:34:52 +00:00
|
|
|
hostport2socket :: String -> Maybe Integer -> FilePath
|
2013-07-22 19:06:47 +00:00
|
|
|
hostport2socket host Nothing = hostport2socket' host
|
|
|
|
hostport2socket host (Just port) = hostport2socket' $ host ++ "!" ++ show port
|
|
|
|
hostport2socket' :: String -> FilePath
|
|
|
|
hostport2socket' s
|
2013-10-07 00:43:22 +00:00
|
|
|
| length s > lengthofmd5s = md5s (Str s)
|
2013-07-22 19:06:47 +00:00
|
|
|
| otherwise = s
|
2013-10-07 00:43:22 +00:00
|
|
|
where
|
|
|
|
lengthofmd5s = 32
|
2012-01-20 19:34:52 +00:00
|
|
|
|
|
|
|
socket2lock :: FilePath -> FilePath
|
|
|
|
socket2lock socket = socket ++ lockExt
|
|
|
|
|
|
|
|
isLock :: FilePath -> Bool
|
|
|
|
isLock f = lockExt `isSuffixOf` f
|
|
|
|
|
|
|
|
lockExt :: String
|
|
|
|
lockExt = ".lock"
|
2012-09-13 23:26:39 +00:00
|
|
|
|
|
|
|
{- This is the size of the sun_path component of sockaddr_un, which
|
|
|
|
- is the limit to the total length of the filename of a unix socket.
|
|
|
|
-
|
|
|
|
- On Linux, this is 108. On OSX, 104. TODO: Probe
|
|
|
|
-}
|
|
|
|
sizeof_sockaddr_un_sun_path :: Int
|
|
|
|
sizeof_sockaddr_un_sun_path = 100
|
|
|
|
|
|
|
|
{- Note that this looks at the true length of the path in bytes, as it will
|
|
|
|
- appear on disk. -}
|
|
|
|
valid_unix_socket_path :: FilePath -> Bool
|
|
|
|
valid_unix_socket_path f = length (decodeW8 f) < sizeof_sockaddr_un_sun_path
|
2013-04-13 22:10:49 +00:00
|
|
|
|
|
|
|
{- Parses the SSH port, and returns the other OpenSSH options. If
|
|
|
|
- several ports are found, the last one takes precedence. -}
|
|
|
|
sshReadPort :: [String] -> (Maybe Integer, [String])
|
|
|
|
sshReadPort params = (port, reverse args)
|
|
|
|
where
|
|
|
|
(port,args) = aux (Nothing, []) params
|
|
|
|
aux (p,ps) [] = (p,ps)
|
|
|
|
aux (_,ps) ("-p":p:rest) = aux (readPort p, ps) rest
|
|
|
|
aux (p,ps) (q:rest) | "-p" `isPrefixOf` q = aux (readPort $ drop 2 q, ps) rest
|
|
|
|
| otherwise = aux (p,q:ps) rest
|
|
|
|
readPort p = fmap fst $ listToMaybe $ reads p
|
2014-04-12 19:59:34 +00:00
|
|
|
|
2015-02-12 20:12:32 +00:00
|
|
|
{- When this env var is set, git-annex runs ssh with the specified
|
|
|
|
- options. (The options are separated by newlines.)
|
2014-04-12 19:59:34 +00:00
|
|
|
-
|
2014-04-29 22:08:10 +00:00
|
|
|
- This is a workaround for GIT_SSH not being able to contain
|
2014-04-12 19:59:34 +00:00
|
|
|
- additional parameters to pass to ssh. -}
|
2015-02-12 20:12:32 +00:00
|
|
|
sshOptionsEnv :: String
|
|
|
|
sshOptionsEnv = "GIT_ANNEX_SSHOPTION"
|
|
|
|
|
|
|
|
toSshOptionsEnv :: [CommandParam] -> String
|
|
|
|
toSshOptionsEnv = unlines . toCommand
|
|
|
|
|
|
|
|
fromSshOptionsEnv :: String -> [CommandParam]
|
|
|
|
fromSshOptionsEnv = map Param . lines
|
2014-04-12 19:59:34 +00:00
|
|
|
|
|
|
|
{- Enables ssh caching for git push/pull to a particular
|
|
|
|
- remote git repo. (Can safely be used on non-ssh remotes.)
|
|
|
|
-
|
2015-02-12 20:12:32 +00:00
|
|
|
- Also propigates any configured ssh-options.
|
|
|
|
-
|
2014-04-12 19:59:34 +00:00
|
|
|
- Like inRepo, the action is run with the local git repo.
|
|
|
|
- But here it's a modified version, with gitEnv to set GIT_SSH=git-annex,
|
2015-02-12 20:12:32 +00:00
|
|
|
- and sshOptionsEnv set so that git-annex will know what socket
|
2014-04-12 19:59:34 +00:00
|
|
|
- file to use. -}
|
2015-02-12 20:12:32 +00:00
|
|
|
inRepoWithSshOptionsTo :: Git.Repo -> RemoteGitConfig -> (Git.Repo -> IO a) -> Annex a
|
|
|
|
inRepoWithSshOptionsTo remote gc a =
|
|
|
|
liftIO . a =<< sshOptionsTo remote gc =<< gitRepo
|
2014-04-12 19:59:34 +00:00
|
|
|
|
2015-02-12 20:12:32 +00:00
|
|
|
{- To make any git commands be run with ssh caching enabled,
|
|
|
|
- and configured ssh-options alters the local Git.Repo's gitEnv
|
2015-11-10 20:52:30 +00:00
|
|
|
- to set GIT_SSH=git-annex, and set sshOptionsEnv when running git
|
|
|
|
- commands. -}
|
2015-02-12 20:12:32 +00:00
|
|
|
sshOptionsTo :: Git.Repo -> RemoteGitConfig -> Git.Repo -> Annex Git.Repo
|
2015-11-10 20:52:30 +00:00
|
|
|
sshOptionsTo remote gc localr
|
2015-08-04 19:27:48 +00:00
|
|
|
| not (Git.repoIsUrl remote) || Git.repoIsHttp remote = unchanged
|
2014-04-14 01:39:04 +00:00
|
|
|
| otherwise = case Git.Url.hostuser remote of
|
2015-08-04 19:44:46 +00:00
|
|
|
Nothing -> unchanged
|
2014-04-14 01:39:04 +00:00
|
|
|
Just host -> do
|
2015-02-12 19:44:10 +00:00
|
|
|
(msockfile, _) <- sshCachingInfo (host, Git.Url.port remote)
|
2014-04-14 01:39:04 +00:00
|
|
|
case msockfile of
|
2015-05-31 02:01:52 +00:00
|
|
|
Nothing -> use []
|
2014-04-14 01:39:04 +00:00
|
|
|
Just sockfile -> do
|
|
|
|
prepSocket sockfile
|
2015-05-31 02:01:52 +00:00
|
|
|
use (sshConnectionCachingParams sockfile)
|
2014-04-14 01:39:04 +00:00
|
|
|
where
|
2015-11-10 20:52:30 +00:00
|
|
|
unchanged = return localr
|
2014-04-12 19:59:34 +00:00
|
|
|
|
2015-05-31 02:01:52 +00:00
|
|
|
use opts = do
|
2015-08-04 20:53:38 +00:00
|
|
|
let sshopts = concat
|
2015-05-31 02:01:52 +00:00
|
|
|
[ opts
|
|
|
|
, map Param (remoteAnnexSshOptions gc)
|
|
|
|
]
|
2015-08-04 19:27:48 +00:00
|
|
|
if null sshopts
|
|
|
|
then unchanged
|
|
|
|
else do
|
|
|
|
command <- liftIO programPath
|
|
|
|
liftIO $ do
|
2015-11-10 20:52:30 +00:00
|
|
|
localr' <- addGitEnv localr sshOptionsEnv
|
2015-08-04 21:07:45 +00:00
|
|
|
(toSshOptionsEnv sshopts)
|
2015-11-10 20:52:30 +00:00
|
|
|
addGitEnv localr' "GIT_SSH" command
|
2015-05-31 02:01:52 +00:00
|
|
|
|
2015-02-12 20:12:32 +00:00
|
|
|
runSshOptions :: [String] -> String -> IO ()
|
|
|
|
runSshOptions args s = do
|
|
|
|
let args' = toCommand (fromSshOptionsEnv s) ++ args
|
2014-04-12 19:59:34 +00:00
|
|
|
let p = proc "ssh" args'
|
|
|
|
exitWith =<< waitForProcess . processHandle =<< createProcess p
|
2014-04-29 22:08:10 +00:00
|
|
|
|
|
|
|
{- When this env var is set, git-annex is being used as a ssh-askpass
|
|
|
|
- program, and should read the password from the specified location,
|
|
|
|
- and output it for ssh to read. -}
|
|
|
|
sshAskPassEnv :: String
|
|
|
|
sshAskPassEnv = "GIT_ANNEX_SSHASKPASS"
|
|
|
|
|
|
|
|
runSshAskPass :: FilePath -> IO ()
|
|
|
|
runSshAskPass passfile = putStrLn =<< readFile passfile
|